I've been experimenting with structural concurrency and using ArrayBlockingQueue to emulate something similar to Go's goroutines and channels in a traditional N:M async producer-consumer setup. Since I'm using these queues to hold tasks and results without needing to return anything, my methods are void. I ran into an issue where I couldn't handle exceptions in the try-with-resources block of StructuredTaskScope, which forced me to return a placeholder (using Void instead of void). It got me thinking: Why doesn't Runnable declare throws like Callable does? Is there a fundamental technical reason behind this inconsistency, making Runnable less user-friendly since exceptions need to be managed within the lambda?
7 Answers
Hey, have you thought about submitting this idea to a mailing list? A solution akin to Runnable that can throw exceptions could genuinely improve things!
You're right that Callable is designed to return results, whereas Runnable is more of a fire-and-forget. But the issue with checked exceptions runs deep. Whether you declare exceptions or not presents a unique set of challenges, and there's no perfect solution.
The best workaround for managing exceptions might be to use the sneaky throws pattern or create a utility that converts a Callable into a Runnable. Checked exceptions were a good idea in theory, but in practice, they make writing clean code really noisy. I’d love a compiler option to turn them off completely!
It’s interesting how Java’s generics entered the picture without improving exception declarations for built-in libraries. You can create your own callbacks and functions that use generics for exceptions, but they come with their own set of limitations.
There’s a JEP on structured concurrency that I think could help with this. It’s worth checking out! But, it might need to introduce a new functional interface that can handle exceptions similarly to Callable but returns nothing.
When Runnable was first created, its sole purpose was to be passed to the Thread constructor. Back then, it made sense for it not to throw checked exceptions. Sure, some might argue that a new thread should be allowed to throw anything, with the uncaught exception handler taking care of it. But at the time, it was a matter of keeping it simple and backward compatible. If Runnable had later been changed to include throws, any existing code that called run() without expecting exceptions would break. So it’s all rooted in that early design choice and a commitment to maintain backward compatibility with Java's evolution.
Runnable was widely used before lambdas really took off. These days, though, many developers are left wishing for a better way to handle exceptions in functional programming. It might be a bit clunky right now, but there’s hope for future enhancements on that front!
Yeah, I think the community is really craving more ergonomic solutions.
Totally! The current state of exception handling in Java seems like a lost opportunity for functional programming.