$ \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
2026-02-23

When is an Exception an Exception

Recently I said that checked exceptions or not really exceptional. Here is a good example about the tradeoffs.

Consider building a directed acyclic graph (DAG), a dependency graph for example. The Node class has a method to add a child:

class Node {
  public void addChild(Node child) { ... }
}

We must make sure that the graph stays acyclic. So the child node may not be an ancestor of the current node. We add a check to addChild.

class Node {
  public void addChild(Node child) {
    if (child.hasDescendant(this)) {
      /* NOT SUITABLE */
    }
  }
  public boolean hasDescendant(Node other) { ... }
}

What shall we do at NOT SUITABLE? We have three options:

  1. Throw an (unchecked) exception (Java, TypeScript, Python) or panic (Rust).
  2. Return a special value, say ISPARENT, to tell the caller that the operation is not possible.
  3. As a middle ground, in Java we can throw a checked exception.

The arguments:

An unchecked exception indicates a programming error, and there are two diferent situations:

So assume we are in the API situation, not in the algorithm-guarantees-non-fuckup situation.

Throwing a checked exception would be a strong hint that the operation may not succeed on circumstances. Yet, as I argued in the article linked above: this is not exceptional. It is normal business?

Which leads us to the special-value return. I started to call those an Explainer. It encodes why the operation was not possible, with as much detail as needed. Examples:

Did you note how I tried above to not say that an operation "failed". Not easy after being brain-washed for 40 years.😀

In languages with union types, like Python and TypeScript, it is slightly easier to return either a result or an Explainer. In Java it would be some Either<Stuff, Explainer> that needs to be defined.

Is there a case for checked exceptions still? The longer I ponder it the thinner the case gets. It seems nice to ignore the explainer (exception) and let it bubble up. Lets compare:

public Either<Result, Explainer> doStuff() {
  Either<String, Explainer> s = compute(...);
  if (s.isRight()) {
    return Either.ofRight(s.right());
  }
  ...
}

with

public Result doStuff() throws Explainer {  
  String s = compute(...);
  ...
}

The latter is obviously more conscise in Java, though the main eye-strainer for me is more the new Either() necessary to match the result type. In a language with union types, like TypeScript, this is just:

public doStuff(): Result | Explainer {
  const s = compute(...);
  if (s instanceof Explainer) {
    return s;
  }
  ... move on with s
}

The advantage of exception forwarding amounts to the avoidance of a mere if/return combo. Yes, you say, but what if there are four for five of those in a row? Then auto-bubbling looks much better — hmm, until you have to debug at what line exactly in the 😠 code the exception is raised.

What if we made explicit forwarding simpler. If we have union types, like in Python and TypeScript, imagine a syntax like:

const text: string = compute(...) or return;

The compiler would unpack this into

const text: string | Explainer = compute(...);
if (text instanceof Explainer) {
  return text;
}

Easy forwarding, explainers need not come along as exceptions and it is obvious were the code did the short turn, eventually. I am dreaming.