$ \def\Vec#1{\mathbf{#1}} \def\vt#1{\Vec{v}_{#1}(t)} \def\v#1{\Vec{v}_{#1}} \def\vx#1{\Vec{x}_{#1}} \def\av{\bar{\Vec{v}}} \def\vdel{\Vec{\Delta}} $

Harald Kirsch

about this blog
2025-06-30

Checked Exceptions are not Exceptional

JavaScript/TypeScript has no checked exceptions and this bothers me.

What are "checked exceptions"

As far as I know, Java introduced checked exceptions whereby a method signature advertises that a method or constructor may throw an exception of a specific type. For example the write method of an OutputStream is declared as:

  public void write(int b) throws IOException {
  ...
  }

When another method calls it, it must do one of two things:

  1. Use a catch(IOException e) {...} clause to catch and handle it, or
      try {
        out.write(0xa);
      } catch (IOException e) {
        // do something reasonable
      }
  2. declare itself also as throws IOException.
      void writeStuff(...) throws IOException {
        out.write(0xa);    
      }

In the first case it is blatantly obvious that calling write() either works or not and this other case is handled in some way by the catch clause.

The second case can be somewhat less obvious if the body of writeStuff gets longer as it is not clear from which call the declared IOexception originates.

Alternatives

Java also provides unchecked exceptions where neither a catch clause nor a throws declaration is required. Nevertheless a catch clause may be written somewhere up the call stack after carefully reading the documentation and/or inspecting the signatures of called methods down the stack in all their breadth to figure out which unchecked exception may pop out.

The unchecked exceptions are what JavaScript solely provides. Any function may throw stuff any time and upwards the call hierarchy you need to do as described above: do some hard work on docs and with your IDEs to figure out whether to better catch something or not.

A third approach is for a method or function to return either the normal result or the "exception". In TypeScript this could be written as:

  function f(in: SomeIputSource): string | IOException {
  ...
  }

In contrast to throwing exceptions, this requires even more explicit action at the caller. It is similar to a checked exception, yet without the automatic re-throw we have if the caller declares throws IOException, as is possible in Java. This is what Rust opted for.

So basically there are three ways to deal with what is customarily called "errors" or "exceptions":

  1. Unchecked exceptions as a machine for surprises up the call stack and, for extra fun, at runtime in production. Coding the caller of a function throwing unchecked exceptions has the "benefit" that the developer does not need to think about it. The tester or the customer will tell if an exception is thrown.
  2. Checked exceptions which allow automatic re-throw such that the caller merely declares throwing the same exception but otherwise no extra control structure is necessary — except, of course ultimately somewhere up the call stack, but not in each function along the stack.
  3. Declaring the method to return either a "normal" result or an "exception". In this case the immediate caller must provide some control structure to handle either one or the other type of result.

There is an ongoing discussion about whether or not to use checked or unchecked exceptions, just do an Internet search. From the wording above you may have guessed that I am not fan of unchecked exceptions. The arguments are all made.

Or not? I think Rust comes close, but let me explain.

These are neihther "Exceptions" nor "Errors"

The Result<R,E> construct used in Rust is a great step the the right direction as it puts the "normal" and the "other" result nearly on par. To improve the base for discussion and guide the thinking I suggest:

Do not call the "other" result type "error" or "exception".

I cannot come up with a better name currently than "other". Nearly 50 years of training nudge me into calling it a lot of things, but all with a negative connotation. But the other result should not be seen as bad, exceptional, error, unsuccesful, failed or whatever. Opening a file with a name for which there is no file is normal business. Why treat it differently than the lookup of a key in a map. It may mapped to a value or not. Both is possible, neither is good or bad, they are on par and should be treated respectively.

The same holds for requesting data from an HTTP server. The resource was changed, the server is down, the network is down, the data format is different from what the software at this point expects. There are gazillion reasons why we might not get the nicely formatted JSON describing the customer address, yet all those gazillion reasons are "exceptional"? Seriously? Maybe getting the nicely formatted JSON should be called the exception. :-)

As another, slightly more tricky example consider the compilation of a regular expression into an internal structure. Two differing use cases can be considered (Java example):

  1. Pattern num = Pattern.compile("[0-9]+");
  2. Pattern grep = Pattern.compile(System.getProperty("pattern"));

The first example uses a pattern literal string provided by the developer. Being forced to handle an alternative result, like a description of a potential syntax error would be extremely cumbersome. Ideally the compiler (as many IDEs do today) would verify the pattern already, but the second best solution in this case, as an exception from my avoidance of unchecked exceptions, would be an unchecked exception or, as in Rust, a panic!.

In the second example, the regular expression is obtained from an external source, like provided by a user, so it is normal business that the string provided is not a regular expression pattern. Rather than just returning, say, null as in the case of a key lookup in a map, it is quite natural to return a description of why the string is not a pattern, customarily called a syntax error.

Java opted for the former, so when trying to compile non-literal strings, the developer is not reminded by the compiler: "hey, the string may not be a pattern". The developer must waste precious brain capacity to remember that this may be the case and add the respective handling voluntarily.

But I think the two examples above show that both use cases are relevant and in such a case I see only one way out: there should actually be two pattern compile function. One with "other" result and one doing the "unchecked exception" or "panic" thing.

Conclusion

I think the debate about checked or unchecked exceptions would be over quickly if we stop calling checked exceptions "exceptions" at all. As soon as we accept that those are just a second type of result, on par with what we call the main result so far, it should become much more easy to recognize whether we really are in an "unchecked exception"/"panic" use case or not.

Personally I'll want to start using the Rust approach in TypeScript too, in particular as union types make this so easy. The explicit handling on each level in the call hierarchy may look slightly cumbersome. Yet: the more explict your code, the less you have to remember or reconstruct when you come back to it in a year's time.

And for quality software, its function must be so blatantly obvious that everybody thinks: wow, this is trivial code, even a monkey could've programmed this.

If your're a junior developer and are proud of "wow, I wrote this complex code and I understand it in every detail", make sure to change employer to avoid looking at this code in some time, because you will hate yourself.