$ \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

Pages

fixed pages table of contents

Blog

site table of contents
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 th signature advertises that function may throw an exception of a specific type. For example OutputStream.write() 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
      try {
        out.write(0xa);
      } catch (IOException e) {
        // do something reasonable
      }
  2. or 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 is less obvious if writeStuff gets longer, as you start guessing where the exception may actually be thrown.

Alternatives

Java also provides unchecked exceptions where neither catch nor throws is required. A catch clause may exist somewhere up the call stack. You may find it after carefully reading the documentation and inspecting the call hierarchy.

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 exceptions, this requires even more explicit action at the caller. It is similar to a checked exception, yet without the automatic re-throw. This is what Rust opted uses.

So we have 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. It 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 with re-throw which makes the thing explicit and, except for the continous re-statement of throws XyzException, the exception can be similarly ignored if you don't want to handle it.
  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 neither "Exceptions" nor "Errors"

The Result<R,E> construct used in Rust is a great step in 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" (Note 2026-01: Explainer seems nice). 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 be mapped to a value or not. Both is possible, neither is good or bad, they are on par and should be treated respectively.

Or consider an HTTP request. The resource was changed, the server is down, the network is down, the data format is different from what the software expects. There are gazillion reasons not to 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 string literal. It should be syntactically correct and handling an "alternative result" 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 to my avoidance of unchecked exceptions, would be an unchecked exception or, as in Rust, a panic!.

The second example obtains the string from an external source, provided by a user, so it is normal business that it may fail to parse. Rather than returning null, as in the case of a key lookup, it is natural to return a description of why the string is not a pattern.

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

The debate about checked or unchecked exceptions would be over quickly if we stopped calling checked exceptions "exceptions". If we accept that they are a second type of result, on par with the "main" result, it becomes much easier to recognize whether have an "unchecked exception"/"panic" use case or not.

Personally want to start using the Rust approach in TypeScript too. And union types make this so easy. The explicit handling on each level in the call hierarchy may look 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 done this.

revised 2026-02