And ones that are so unergonomic that only industrious developers will get them right will result in worse software.
Modern languages allow these to be zero cost abstractions so there is little tradeoff.
For instance the IO monad, which is used everywhere Scala/Cats. It can contain a result or an error. If you don't feel like checking for an error after you called some method, you can just pass it up and return the IO from your method. Does that sound familiar? It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.
Then it behaves like an unchecked exception. Which is fine.
'Checking' is the act of modifying your source code to declare the error.
No. If I add a checked UserNotFound exception to a getUser db call, you can bet someone higher up the stack will do try catch Exception e, so now they're catching OutOfMemory and who knows what else.
come on, it's not as if anyone disagrees with that, but that's extremely, extremely rare, overwhelmingly, callers are just dealing things like with UserNotFoundException, NullPointerException, what have you, and there's no reason why the compiler should happily let you catch Exception (or nothing) when it could just be giving you an honest object back
But that's laziness on the caller's part. If I offer a method but the caller decides to do reckless lazy crap with it, there are many different ways to get there in any language. I typically call those out (Exception e) at code reviews.
There are countless examples of rare but legit non-obscure use-cases. And even if your code is fine, you can't expect the same for the libraries you are using.
And some exceptions make frequent non-happy paths more visible. Most of all IOException, because IO can _always_ fail for all the wrong reasons (because the failing of this exception is outside of the JVM's influence, it is rightfully a checked exception). And often you simply don't want to do the error handling at call-site but propagate to the code which is controlling the use-case.
But more importantly, force-unwrapping is not equivalent to catching generic exceptions. Instead, it's equivalent to catching all checked exceptions and wrapping them in a Runtime error. It's also almost equivalent to what this compiler plugin does (or Kotlin or Lombok's @SneakyThrows do).
Catching "Exception" and trying to handle it generically, is more closely equivalent to this type of code:
match result {
Ok(value) => doSomethingWithValue(value)
Err(e) => println("Error: {e}!")
}It’s got nothing to do with getting better. It IS basic Java exception handling. Any proper course or tutorial will tell you to catch specific exceptions
any real world library or application will catch Exception e, catch checked exceptions and turn them into unchecked ones etc etc
I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method. For example, some older Java APIs take an encoding name and force you to handle the checked UnsupportedEncodingException - even when the encoding name was something like “US-ASCII” or “UTF-8” which is required to exist by the Java standard, and if somehow they didn’t there is often no possible error recovery than just crashing. This has been fixed in newer versions by introducing new APIs which throw an unchecked exception instead, and also by introducing constant objects for the standard charsets
If I want to ignore a set of exceptions, I have the option to catch(Exception e) {} signaling that I recognize the risks that have been explicitly communicated by the API (that throws). An @IgnoreExceptions annotation would help dump the 5? boilerplate lines.
The unknown risks for other non-specific Runtime exceptions, are not included. I can catch those too if I add a catch(RuntimeException e){}, again signalizing that I recognize the risks such that other developers understand that I'm opting out of handling those conditions, which may or may not be errors in a classic sense. eg an expected socket not being available causing an IOException, because I'm doing some concurrent process.
Without checked exceptions, you end up with far fewer catch clauses, and hence far fewer opportunities for buggy catch clauses. I think in most apps, most of the time, you just want to bubble all exceptions up to a central point which handles them – for example, in a web server, you can have a single catch clause for the whole HTTP request. If need be, you can make that central point extensible with custom handling with specific exceptions (like JAX-RS ExceptionMapper, or @ExceptionHandler beans in Spring – which you can then inject using IoC/DI). Once you've got that working, if you need catch clauses deeper in the code (e.g to return an error code to the frontend when the client sends bad data instead of just failing the request with a `NumberFormatException`), you can add them where appropriate – but unlike checked exceptions, you aren't forced to add them where you don't need them.
I've seen quite a few in for-loops, so I'm not sure that's tracking for me.
> Why? Because they encourage you to put catch clauses everywhere
Java forces you to handle author-specified error conditions (CS error), but jr developers do tend to create their own more often than necessary.
While I agree that a global exception handler is good practice for Spring applications, individual exception handling is very common and important to modern software. eg If Web APIs get more complex (multiple datastores), you find you don't want to bubble everything up. I get a request, want to check a cache (which might throw) then look it up in a DB (which might throw) then look it up in a file (etc), then return a response.
I do wish I could handle exceptions simpler than making the choice to add 4 lines (+ any actual logging, etc) or blackhole-traveling through the stack to add checked exception signatures everywhere (code AND tests).
The difference is that for-loops are almost an essential language feature – the vast majority of languages have them (or a close equivalent), and they make certain algorithms a lot easier to state clearly. Sure, there are some languages which lack them, but they tend to be either languages with non-mainstream paradigms (such as pure functional or logic programming languages) which put all the emphasis on recursion instead, or else really old legacy languages which predate the development of structured programming (such as pre-1980s versions of COBOL–modern COBOL versions have for loops)
By contrast, almost nobody considers checked exceptions an "essential language feature" – languages which lack them vastly outnumber languages which possess them, indeed, Java stands out as the only major mainstream language to possess them
Given the argument "this unusual inessential language feature causes more bugs than it prevents", the response "this essential language feature which the vast majority of languages have sometimes causes bugs too" isn't very convincing
Good developers should be lazy. Checked exceptions require you to do a bunch of cumbersome ceremony that makes your code unreadable, for what should be (and is, with Either or equivalent) at most a couple of symbols; no wonder developers hate that.
> I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives.
You'd be wrong. Checked exceptions as implemented in Java require two keywords of their own (throw and catch), a special change to method signatures (throws), a unique new kind of expression for multi-catch (|), a unique new kind of type for things caught in multi-catches, and special support in every other new language feature (e.g. Futures have a whole bunch of extra code to deal with exceptions). Eithers can literally be a regular class that you could write yourself using ordinary language features. A small piece of syntax sugar (something like "do notation", "for/yield comprehensions", or "?") is a good idea, but not essential, and if you do it right then you can implement that once across your whole language (for applicatives/monads in general) and use it for eithers, futures, database transactions, audit logs, almost anything. https://philipnilsson.github.io/Badness10k/escaping-hell-wit... .
try {
throws_a();
r = grab_resource();
throws_b();
r.throws_a();
} catch (a) {
r.release(); // oops; null pointer some times
} catch (b) {
try {
r.release();
} catch (a) {
// nooo....
}
} finally {
if r != null {
r.finalize() // use after release (sometimes)
r.release() // ???
}
}
There is plenty of academic literature showing that real programs are even worse than my contrived example, on average.Quite sure Java would prevent that if r was never assigned.
Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.
Context managers (try with resources) specifically don't work at all when release is conditional e.g. open a file, need some post processing, need to close the file if the post-processing fails but return it if it succeeds.
Defers don't either, unless you have a specialised one (e.g. zig's errdefer).
Actual RAII (aka affine types) does work, but adding that to a GC'd langage after the fact is a huge undertaking and increase in complexity, because the interaction between affine and normal types is fraught.
The paths are still there if the exceptions are all unchecked or combined or not using exceptions at all. Passing exceptions up is rarely the right choice, imo. If it's a terminal runtime, sure, obv.
In my book, if anything, they are much much better! Unfortunately they don’t have a flawless implementation, but hopefully languages with first-class effects will change that.
No, they aren't.
They are not compositional. You may want to write `f(g())` but there's no way to write the parameter type of `f` to make this work (in Java). That's because checked exceptions are an "effect" that would require extending the Java type system.
You might do it because you want to factor the handling of the different result cases out to another function.
It works great. Also, note that f() doesn't care about the type of error returned by g(), and that it will be a compilation error if g()'s error type turns into something incompatible.
Sadly, there are proposals to add exceptions to rust, and it seems likely they will be accepted, breaking error handling semantics across the entire ecosystem (even in new code, since exceptions are too hard to use correctly).
AFAIK, the only proposal related to exceptions is adding a `try` block that would scope the `?` operator to that block instead of the current function.
In a better langage f would take a Result, and then it can manipulate that however it wants.
Obviously you can also plug in adapters if you need some other composition e.g. g().map(f), g().and_then(f), …
You can – by making f accept g's return types, including the error. This is even being done in the Go standard library: https://pkg.go.dev/text/template#Must
In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).
Java has three types of errors:
1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.
These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.
Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.
Consider the following example: We have a purchase() function which can return one of the following errors:
- InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.
You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.
In Rust, you would do something like this:
account.purchase(request).map_err(|err| match err {
PurchaseError.InsufficientAccountBalance(available, required) => {
account.auto_top_up(required - available)?
account.purchase(request)
}
_ => err // Do not handle other error, just let them propagate
})
In Java, you would generally do the following: try {
account.purchase(request);
} catch (InsufficientAccountBalance e) {
account.auto_top_up(e.requiredAmount - e.availableAmount);
account.purchase(request);
} catch (Exception e) {
// We need to catch and wrap all other checked exception types here
// or the compiler would fail
throw new WrappedPurchaseException(e);
}
The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?
This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.
In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.
I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling.
In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying.
I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.
Which is bad. In 99% of cases I have no specific error handling for the given problem. Just let the process crash or be handled by application server / framework.
Representing these kinds of error conditions in the code which I have no interest in handling is just noise.
> I have no specific error handling for the given problem
then pass it on and decide what to do with it at the system boundary
in the db:
fun getUser(uuid: UUID) : Either<Error, User>
middle layers: pass around the either, you can map, flatmap etc on it to chain it with other computations there
then in the resource layer
return user .fold({ error -> when(error) { is DbError -> HttpResponse.InternalServerError() is UserNotFoundError -> HttpResponse.NotFound } }, { user -> HttpResponse.ok(user) })
Then, at every layer, each method explicitly says in the method signature what it returns. There’s no need to look around each and every line of every method in every layer, or in the framework, or some global exception handler, to figure out what will happen. Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.
Things like being unable to communicate with the DB or not finding a user are not exceptional, they're entirely expectable when you're calling a db to look for a user and should be modeled accordingly.
Why? In the simplest (but pretty common) case it's:
- successful response
- generic error message
> then pass it on and decide what to do with it at the system boundary
Yes, but if in 99% of cases I only pass it on, it's just visual noise. Noise you become blind to, and it loses meaning.
> Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.
They can't really because you end up with some very generic error type anyway. Any typical service can have IOError, DBConnectionError, SQLError, OutOfMemoryError, StackOverflowError plus many other application specific errors. You end up with bulk of your methods returning Either<Error, Something> which is then meaningless.
Your controller method cannot act differently on a DBConnectionerror or OutOfMemory error.
Not to mention that exceptions cause developers to use them as control flow mechanisms.
For example searching a user by id. If the database returns 0 records, is that reason to throw an exception? Well, doing result[0] results in IndexOutOfBounds due to result being [].
But the reality is that the user not being there isn't exceptional. Typos are common. By using Result<T, E> or Either you enforce the developers to think more about their flow. One can write the method like this:
fn find_by_id(id: usize) ->Result<UserResult, Error> -> {
let raw_db_result = db.search("SELECT id, first_name, last_name FROM user WHERE id = ?", id)?;
match raw_db_result {
None => Ok(UserResult::NotFound),
Some(r) => {
let only_user = r.ensureOneResult()?;
let user = mapDBUserToUser(only_user);
Ok(UserResult::User(user))
}
}
}
What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.A 404 is actually a valid result.
Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.
Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.
I'm not defining the errors like DBConnectionError or OutOfMemoryError - it's the framework/platform which defines them and throws/returns them.
> But the reality is that the user not being there isn't exceptional.
That depends. In some contexts it is not exceptional (getting user by ID given as an argument to webservice), in that case using Maybe type is great. In other contexts it is very much exceptional (e.g. signed JWT refers to a non-existing user) and throwing Exception makes more sense.
> What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.
Which is the same as exception. But now you have this visual noise of something you claim you don't care about. An unchecked exception makes this irrelevant noise go away.
> Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.
try {
...
}
catch (UserNotFoundException e) {
// handle ...
}
I'm catching exactly the exception I want. Where I'm catching too much? Where do I need to take it apart?(This particular example of exception usage is bad, though, as it smells of exception control flow. Here using Maybe type would be better)
> Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.
Which is good for some cases where the enum describes "business" cases.
But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway). Library adding a new exceptional case will break its clients, which seems like a bad trade-off (again, for truly exceptional cases).
try {
...
}
catch (UserNotFoundException e) {
// handle ...
}
```This is just bikeshedding, because the equivalent code in Rust would be. For example, if there's multiple lines in the try block, who exactly returned this error? Are there other errors I didn't handle? Are there unexpected error that I forgot to check. For example, the "get" function of arrays in many languages usually always return T. But this is actually a lie because the array might be empty. So by right, it should return Option<T>. But exception based programming have basically created this illusion that its infallible. How many people check their array accesses?
```
match value {
Ok(ok)=> {...}
Err(UserNotFoundException(e)) = { handle }
e => return e
}
```
Which does look more complicated, but it scales way way better when you have multiple errors
> But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway).
But why should a language be designed for exceptional cases? Errors are not exceptional at all. In the above code, the actual code will actually look like this
```
let rows = db.get_rows(query)?; // returns Result<Vec<DbRow>, E1>
let first_row = rows.first()?; // returns Option<DbRow>
let user = first_row.to_user()?; // returns Result<User, E2>
return user
```
Exception-based language will have the same looking code, but then imagine what would happen if you try to figure out which functions return what. You have no other recourse other than to dig into the source code to find all the unchecked exceptions that it can throw.
Another example, how would an exception language write this code to get multiple rows from the db and map each row to its respective user.
```
// get_rows and to_user both can fail
let users :Vec<User> = db.get_rows(query)?.map(|r|r.to_user()).collect::<Result<_,_>()?;
```
You cannot catch a StackOverflowException or an OutOfMemoryException. All you do is log it & restart the app.
Once could split it into retryable errors and non-retryable ones. Like a DB disconnect, that can be retried.
BUT to be fair, that's not logic that belongs in the business.
fun getUser(userId: UUID): User?
You cannot treat this result value as a `User` in code that calls this; though, once you null check it in your service layer, you can pass it on as `User` in the not-null branch. Null is nothing to be afraid of if the language forces you to declare nullability and deal with it.on the other hand, depending on what your trying to do you might want to provide more context about what happened to the user/programmer
in swift you can change a throwing function to a nullable with `try?` so even if `getUser()` throws, you can keep it simple if thats what is appropriate
guard let user = try? getUser(id: someUUID) else {
return "user not found"
}
as an aside, swift "throws" exceptions but these are just sugar for returning basically an Result<T,E> and compose much better than traditional stack unwinding exceptions imoThat said, not modelling them in the type system is a mistake. But the model has to be useful - knowing what functions can or cannot throw is useful, knowing what they throw, less so.
(They are safe because of try-finally / try-with-resources)
That's what wrong with them. Callers, in almost all cases, do not know what to do exceptional outcomes. So you force programmers into a situation where they're dealing with things they shouldn't be dealing with.
Functional error handling types are much simpler, safer and more powerful.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
> What is wrong about dedicated syntax
It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
> Exceptions are easier
But that's the point, they're not.
Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.