And I don't think this would actually be that bad. Functions/methods could still decide not to propagate exceptions outward; they just have to declare what they throw/propagate.
Of course, the problem here is that the list of thrown exceptions becomes a part of a function's API (and possibly ABI); changing the implementation details of a function -- even without changing its behavior -- might require you to add a new exception to the list, which would be an API break. I expect this would lead people to often declare that their functions throw the generic base-class Exception type, which isn't great.
This means that checked exceptions and explicit error returns that are anything other than a very generic error type are incompatible with encapsulation and polymorphism.
I think the best generic solution to error handling is unchecked exceptions. Most code should not be littered with try/catch blocks and none of them should be gratuitous. Even in my largest projects the number of catch blocks can be counted on one or two hands. You don't need to know what might or might not throw -- everything can throw. If something does throw today, it could throw tomorrow. Don't worry about that. With unchecked exceptions, just concern yourself on what you can handle and where you can handle it. Most error handling is either to abort (the operation or application) or retry.
This doesn’t mean that the function signature has to expose implementation details. If the function is file_open(), listing FileNotFound as one of the possible errors doesn’t expose an implementation detail. If the function is sql_insert(), distinguishing between ConnectionError and UniqueKeyConflict is useful and doesn’t expose implementation details. Different error modes like that exist on all levels of an application.
I don't disagree but most of my application is not in those leaves; it's a call stack 20 levels deep.
The correct thing to do in that case is to wrap/convert those errors to errors that are appropriate to that function’s interface contract. Exception types are an important part of a function signature, and should be designed accordingly, not be propagated blindly.
With unchecked exceptions, you are leaking implementation details dynamically, and you don’t even know it/can’t control it, because they are not statically checked. If you care about leaking implementation details, unchecked exceptions are arguably worse.
> I don't disagree but most of my application is not in those leaves; it's a call stack 20 levels deep.
I regularly have such failure mode distinctions in higher-level program logic as well. With unchecked exceptions, you often aren’t even aware what failure modes you have.
Those, however, tend to be useless. What you end up doing is placing the real error untyped into new the exception/error and that's where the real meat is.
I can create an application that could retry an operation if it sees a particular kind of network error. Does it also work for the dozens of custom errors used by the interface contract of every single method? That's impossible to even know. How do I handle dozens of different SomeModuleError types?
> you often aren’t even aware what failure modes you have.
Again, that is the point of encapsulation and polymorphism. Maybe the implementation doesn't even exist at compile time in the case of a plugin.
I don't want to have to re-write my entire application every time some implementation detail changes anywhere. If you don't want to do that, then you're have to transform your errors/exceptions into something useless.
What I want to focus on what kinds of errors I can handle and where I can handle them. That has nothing at all to do with where the errors are generated.
Because it represents the conclusion, despite ample guidance and dogma by Sun, of the mass market millions of java programmers, resulting in second generation (groovy) and third generation (kotlin) JVM languages that dropped checked exceptions entirely.
It might be the languages error handling syntax (try catch) led to ugly code (and I always contended that local vars declared in a try block should have been lexically visible to catch blocks)
It might be that javas std lib had a poorly designed exception pattern. It could be that implementing exception classes was annoying.
But I think there is other reasons. In large scale enterprise development, checked exceptions start to pollute and permeate code that otherwise doesn't want excessive type system exposure from the linked /imported libraries.
What results even with checked exception lite is that "exception barriers" that catch all exceptions and repackage them are established at major system boundaries. Checked exception enforcement would just create more of these IMO.
The wet dream of checked exceptions was encouraging APIs that had a thorough understanding of all the error conditions and could use checked exceptions to enforce a good handling recovery API usage pattern
In reality it's the halting problem: too hard to predict l all the error conditions (that isn't really known until version 3 or 4 of the API and by then the API signatures are set.
Let's look at something simple like json serialization. Yeah there's a litany of parse errors and the like.
But there is ALSO the issue of disparate sources of the json. File errors from file sources. Network errors from streamed communication channels. Your general stream or file interface isn't that simple of an abstraction when source specific errors raise their heads.
The abstraction become leaky real real fast.
I too would like to live in this world.
However, while I don't have specific exceptions at the tip of my tongue, Even sun failed in many of their jdk apis to produce a good checked exception design.