The Trouble with Checked Exceptions (2003)
artima.comI like James Iry’s take on checked exceptions here[1] which basically boils down to this:
> The throws clause is the only point in the entire Java language that allows union types. You can tack “throws A,B,C” onto a method signature meaning it might throw A or B or C, but outside of the throws clause you cannot say “type A or B or C” in Java.
In languages with better support for ad hoc union types, I think both the need and desire for checked exceptions fades. I wrote a little bit more about how two translate between the two concepts and what the benefits of using ad hoc union types are in this post in a blog post[2], but my point is that having a proper, fully fledged type system feature that composes with all other features of the type system is what drives most of the headaches away.
[1]: https://james-iry.blogspot.com/2012/02/checked-exceptions-mi...
It is pretty expensive performance vise
Is it? The VM is already already storing the tags on objects to be able to down cast. For example, in Java, the JVM is already storing the bits to remember that your `x` is a `Child` to safely raise an exception for
So at least it's not expensive from the standpoint of "uses more memory."Child castToChild(Object x) { return (Child)x; }Maybe you're saying that it's more expensive because dynamic dispatch on a type like (A | B) will be slower than static dispatch on a type like A, but most languages lacking ad hoc union types will already have other ways to do dynamic dispatch (parent classes, interfaces, etc.) so it's not like adding ad hoc union types solely incentivizes people to write slower, dynamically dispatched code.
Maybe when you say "performance" you mean that the performance of the type checker is slower? That's definitely true—as ad hoc union types get larger (dozens, hundreds, heaven forbid thousands of variants) the type checker will definitely struggle.
That being said, there have been some impressive optimizations in practice to speed these up (I've spoken with members of the TypeScript team touting some really cool improvements to their union types, and I've seen members of my team submit similar but not quite as good improvements to the union types in Sorbet for Ruby). So I definitely agree that a priori, they're going to be slower to type check, but I think that they can be made fast in common cases, and their benefit in terms of power added to the type system is just a really, really good tradeoff.
So I'm not sure I agree on face value that adding ad hoc union types to a language necessarily come with a performance expense.
My understanding is that it is not the exceptions themselves, it is filling up stack traces which is considered performance taxing thing to do in jvm
Depends on how it is compiled. There’s no reason in principle the two can’t have the same performance, it’s just a difference in ergonomics.
class A extends UnionBase;
class B extends UnionBase;
public UnionBase someMethod();
furthermore, UnionBase might have isA() and isB() if need be.
So, strictly speaking, your statement is incorrect.
In this case UnionBase is not ad hoc: you had to declare a name for it up front. Ad hoc means you can write the union type wherever types can be written. For example, it’s not possible to write this:
in Java.public (A | B) someMethod();Yes, this is the basic benefit of typed languages. I would not want to refactor a 20 year old code base without type safety - I do want to know what's being returned.
I also want my tools to know what's being returned, as well as my compiler.
But in languages supporting this you do know that it's either A or B and you can use match expressions to deal with both cases.
Usually these are the languages with more focus on type safety, like OCaml, Haskell and Rust.
To be fair, in OCaml, Haskell or Rust the type "A | B" also had to be declared beforehand.
Not exactly. For example, in Rust, you can do unions for traits: https://play.rust-lang.org/?version=stable&mode=debug&editio.... OCaml has the same system for its objects, where objects are structurally typed.
Yes, but it doesn't really change the way you could handle it, and what I wanted to point out is that it actually helps with type safety, enabling you to create total functions.
> Bill Venners: But aren't you breaking their code in that case anyway, even in a language without checked exceptions? If the new version of foo is going to throw a new exception that clients should think about handling, isn't their code broken just by the fact that they didn't expect that exception when they wrote the code?
> Anders Hejlsberg: No, because in a lot of cases, people don't care. They're not going to handle any of these exceptions.
As an argument against checked exceptions, this is nonsense. Hand-waving away making breaking changes to an API because "people don't care" about handling the error conditions. I consider it a sign of the immaturity of the software industry that "proper error handling was too much work, so we didn't" is an acceptable sentiment, let alone an acceptable practice.
When writing a programming langauge, your users are programmers.
You can wish they had different behavior than they have, but wishing wont' make them do it. At the present point in history, the interviewees say, there is lots of evidence of what programmers actually do with checked exceptions, and that's what they are speaking to, very explicitly. What they actually do are things "That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn't help anybody."
Or as Hejlsberg also says: "You see programmers picking up new APIs that have all these throws clauses, and then you see how convoluted their code gets, and you realize the checked exceptions aren't helping them any."
So, what you maybe need is a different design which the developer-users of the language will actually use in a way that adds value. It's unclear what this different design is, it hasn't been done yet, they say.
Heljsberg again: "Once a better solution is known—and trust me we continue to think about it—we can go back and actually put something in place. I'm a strong believer that if you don't have anything right to say, or anything that moves the art forward, then you'd better just be completely silent and neutral, as opposed to trying to lay out a framework."
The current practice of checked exceptions, as evidenced by what programmers actually do with them, is not helpful. They lay out a case for this. It could be a "sign of immaturity of the software industry" that a helpful design hasn't been found, I dunno, but that doesn't make it any more helpful to do the thing that hasn't been working.
This is a very reasonable argument.
The way to argue against it would be, I guess, to say that they are wrong, many developers DO use checked exceptions in a way that adds value, and provide evidence for this. Or, perhaps, to say that they are right that MOST don't, they may even be right that for MOST actually existing code checked exceptions degrade the quality of code (that is their argument), but that there are a minority of developers who use checked exceptions right (and provide examples of this), and say that justifies putting them in a language. You could argue that, but you'd have to argue it.
> what programmers actually* do with checked exceptions*
If the only trade-off considered is 'writing "gobbledy gunk" to handle exceptions poorly' vs 'not handling them at all' then, of course, checked exceptions will look bad.
If the trade-off is whether the compiler will be able to tell me that I'm not handling an exception, well, I want the compiler to help me out. People writing low-consequence code can sprinkle "throws Exception" everywhere.
Without checked exceptions, it becomes my job to ensure that exceptions are handled, and I am nowhere near as thorough as the compiler.
> what you maybe need is a different design*
Could be. Perhaps exceptions on the whole are not a good error mechanism because of the way programmers think about them.
Picking two HN favorites, Rust has Result<T, E> and Zig has error sets, both of which effectively work a lot like checked exceptions but with better syntax (and without the bad rap). try/catch is clunky by comparison.
The crucial difference with Rust's Result (I don't know Zig well enough) is that Result isn't about control flow.
Exceptions unavoidably and deliberately change control flow.
Suppose you're in a loop twiddling zarks. In Rust, twiddling a zark gives you a Result and if the Result isn't Ok then that's an error. But Rust doesn't care what - if anything - you do with the error, it just won't allow you to pretend the error was Ok (because it isn't). You can count up all your Results and consider what fraction were Ok. You can filter out any that weren't Ok. You can ignore the Result altogether. Or, if you find one that isn't Ok you could give up twiddling zarks immediately. Rust doesn't mind, Results are just data, do whatever you want with it.
But in a language with exceptions, checked or not, each time there's a problem twiddling a zark the exception jumps the program to somewhere else to "handle" the exception - and it's on you, the programmer, to manage that, by e.g. wrapping the zark twiddling in a try-catch block.
Error returns and checked exceptions are isomorphic. There's nothing you can do with one that you can't do with the other.
But I agree with the underlying point you're making. The error mechanism that a language provides determines how a programmer will think about handling errors. Exceptions make you think about error handling as control flow. Error returns make you think about error handling as unwrapping data.
Which is the imo correct approach. Instead of unwrapping and rewrapping the exception at the closest level, I want to handle most exceptions in a common place. Eg, in case of a web server that place would give a proper error page to the user whenever an unhandled exception happened in his/her request.
Also, Java’s exceptions are pretty much the exact same sum type as in Rust, just with added syntactic sugar. A method with a throws SomeException signature will have a type of Result<Something> | Error<SomeException>, that gets autounwrapped for you, with the added benefit of giving you a stacktrace by default.
> Java’s exceptions are pretty much the exact same sum type as in Rust, just with added syntactic sugar.
Except Java doesn’t actually return a result object that can be assigned to a variable by default.
And in case of exceptions, why would that be useful?
(For other things, I agree that sum types are cool, and fortunately are supported in Java through sealed classes)
I don’t think sealed classes offer exhaustive pattern matching. Instead you have an if-else ladder of instanceof checks. This is not the same as most union type implementations.
They do with switch expressions. It is only type based for now, but deconstructs are coming with full blown pattern matching a la Haskell.
Hejlsberg said it multiple times: Most software developers handle exceptions centrally, e.g. around their event loop.
If an API really throws a new exception which can be handled otherwise than displaying "something went wrong", then I dont think its the task of the programming language. This should be in the release notes.
> Hand-waving away making breaking changes to an API because "people don't care" about handling the error conditions
Generally, adding more specific exception types, so long as they are subclasses of exception types already handled, would not be a breaking API change. Your general exception handler would still run, and you could choose to do something special with the new exceptions if that helps your calling program be better.
Changing how a certain error case is represented such that a previously triggered handler is no longer triggered might be considered breaking, though.
I think exceptions are a not what you should use for regular error conditions that you can handle with the caller. ML descendants are great for that. Sum types with pattern matching to make sure the caller handle the regular error conditions, and exceptions for "catastrophic" things. This with some tooling to show you where exceptions can bubble would be perfet for me.
An old but goodie. While Hejlsberg touches on a fair amount of good points in this discussion, I’ve never agreed with the end result.
He went on to make Typescript - making sure that JS has types, so when making method calls you could have the compiler tell you when you did something wrong. Again, errors/exceptions did not get a throw clause, making it impossible to model the error state that the language provides. You can describe, in detail, the data for successful run, but are unable to describe the data for any error state. If the goal was to make the system more sound, I have a hard time seeing why the error state is not taken into account.
A lot of the described (and for myself experienced) cases against checked exceptions comes from external libraries or the language itself. IOException has a special place in hell, but for my own business logic (or inherited legacy code) in an application that spans thousands of files and classes, I would very much like to describe and be made aware of the error state throughout my application.
In Java, if I introduce a new business logic error state, all the places in my http (or cli or whatever) layer that uses that business logic will get highlighted for me, and I can then map it accordingly. In C#, that is not the case. If I don’t want checked exceptions, I can always catch it and throw a runtime exception - but I have the option. As a developer who likes to think I know what I’m doing, I would at least like the option. In C#, that decision was never mine.
With TS, we have a system that lives around being configurable. One day, I would really like to see this being up to the developer and a parameter in the tsconfig file, maybe with a configurable list of error types to be considered RuntimeExceptions. That way, if I don’t agree with a library’s use of the feature, I can tell TS to not report them by considering them Runtime.
Might just be because I use an architecture that actually favors CheckedExceptions, but not being able to describe the error states of my business logic really grinds my gears ;)
> Again, errors/exceptions did not get a throw clause, making it impossible to model the error state that the language provides. You can describe, in detail, the data for successful run, but are unable to describe the data for any error state. If the goal was to make the system more sound, I have a hard time seeing why the error state is not taken into account.
I think the idea is that exceptions are too hard to handle without going too far from JS, so you're supposed to handle errors Go-style, but instead of [result, error] you can return result | error thanks to union types. TS is here to statically type JS, but it can't check for exceptions, so pushing people to use them would greatly reduce the value of the tool.
I've said this in another comment, but I really think sum/union types can give you 90% of the benefits of checked exceptions. In the article, `FileNotFoundException` is mentionned. That's something that could be handle with the function returning File | FileNotFound. You can achieve this with sealed classes in C# and Java, but it sounds painful to do this every time. Like someone else said, with checked exceptions you have ad-hoc unions, instead of having to create them yourself with sealed class. Since you have neither sum types, nor unions in Java or C#, checked exceptions in Java are your best tool to handle your case.
I feel a lot of the problem here is that someone make the FooLibrary that does some file system call and sometimes throws some IOError. Later on a new backend goes over the network and it throws a NetworkError now too.
This is a leaked abstraction. These are both a kind of FooError. If you’re using a strong enough type system that cares about these things, you should be using it to invest in that sort of strong encapsulation, too. Each API layer should be a translation layer.
Needless to say, this is a fair bit of work, and takes a fair amount of rigor, so it usually won’t happen.
I 95% agree with you. But I think it's just slightly more nuanced. If you tell FooLibrary to open a FooFile, then throwing FileNotFoundException is acceptable to signal that that file is not found. In a sense it's just passing along an exception that was meant for you.
On the other hand, if FooLibrary tries to read some config file or fetch metadata over the internet, then that's a sort of detail that it shouldn't bother the caller with. It should either handle the exception or translate it to a FooException.
> You don't want a program where in 100 different places you handle exceptions and pop up error dialogs. What if you want to change the way you put up that dialog box? That's just terrible. The exception handling should be centralized, and you should just protect yourself as the exceptions propagate out to the handler.
That's assuming a definition of an exception as a generic error you can do nothing about except tell it to the user and maybe retry the operation or kill the program. But in practice exceptions are used for more than that, they're a sort of union type where a method can return a result or an error, but because Java didn't have union types exceptions were used to signal that. For example parseInt will throw a NumberFormatException if it can't parse the string you pass as argument (NumberFormatException is unchecked but it really should be checked).
I think it's the coexistence of these 2 different ways things can go wrong under the same term that is the root of the confusion around Exceptions. The first way is using an exception as a normal outcome of a method, like parseInt above. Using the word "exception" for that is wrong, it is not exceptional for parseInt to return an error if you pass to it some string that does not contain a number. It is expected and will happen every time. The type system should be able to represent that with an union type or Either<> or something of the sort instead.
The second way things can go wrong is when the program is put in contact with the outside world, and the outside world is messy so things will happen that are not what you expect. You try to open a file but it cannot be found any more, you try to allocate some memory but the operating system refuses to do so, some network call is taking too long and so on. Those will have to be dealt with case by case, sometimes you can do something about it, sometimes not. I know programmers don't like that very much but I don't know what can be done about it, it's just the nature of dealing with all the edge cases.
Exceptions are exactly the same thing as sum types, there really is no difference between them on a conceptual level. They just come with additional stack traces and good, no boilerplate syntactic sugar in Java.
> Adding a new exception to a throws clause in a new version breaks client code.
Altering the return-type of a new version also breaks client code. Both return-types and thrown-types are kinds of results from your code, and both should be treated with similar rigor.
The main difference is that programmers are forced to get one of them right early on (in order to get their demo working) while the other is often ignored until late in development when you start needing to handle errors.
Conversely, always using unchecked exceptions "because it's too much work" is the same tradeoff as writing like writing methods that promise to return Object. Sure, it frees you from needing to plan, but it also makes it much harder for you (or another developer) to plan the next thing, leading to a snowball effect.
The Java implementation of checked exceptions suffers from the scalability issue as described. As long as each consumed package wraps its dependencies checked exceptions into not too many kinds this could be workable.
In practice though this isn't how it goes and exceptions in signatures grow based on changing versions of dependencies and their dependencies so either exceptions signatures change by version, or lie.
Two other things that mess all of that up if you manage to get it right: (1) RuntimeExceptions which ought to be checked, e.g. NumberFormatException and other common (not so exceptional) cases, (2) sneaky throws that subvert the unsound type system, which can be convenient to work-around the problems above but leave you with less confidence overall.