Skip to main content
1 of 2
kaya3
  • 20k
  • 40
  • 120

There are two aspects of this ─ one is the idea of having homogeneous exceptions, the other is the catch statement which cannot filter what it catches.

No catch filter

You say that Rust does error-handling without inheritance, but inheritance is a red herring here. Your proposal is to have just one exception type, whereas languages like Java allow multiple exception types via inheritance. But Rust allows multiple error types via composition instead; Result<T, E> allows any type to represent a failure condition, including enums, which are easy to filter with match.

Rust doesn't need errors to have a common supertype, because it doesn't automatically generate a stack trace when you construct an Err(e). In languages like Java where stack traces are guaranteed, exceptions must inherit from Throwable, so that they invoke its constructor which builds that stack trace. Rust's choice means you don't get stack traces unless you panic!(), so errors are either recoverable or can be debugged without stepping through the code, but not both.

In contrast to both, your proposed language does not allow different error conditions to be represented by different types, nor does it allow the catch statement to filter by the error condition. This encourages the "catch, check and rethrow" pattern from Javascript:

catch(e) {
    if(!(e instanceof FooError)) { throw e; }
    // ...
}

Javascript makes this bad enough, because people routinely forget to opt out from the exceptions they don't intend to handle. But in your proposal, instanceof checks are not even possible; you would have to compare the numeric error code.

Numeric error codes

These are considered harmful. Using integer constants as the preferred way of telling apart different error conditions was encouraged by C, but there are good reasons modern languages have abandoned them. (PHP used to have them, with many of the same problems.)

Some of these problems are mitigated by your exceptions also carrying strings, but this could cause problems of its own ─ particularly, it encourages comparing errors by strings instead, which will cause code to break if those strings are ever changed (e.g. to add clarifications or fix typos).

Numeric codes are harder to filter than types

Suppose there are several different error conditions which can occur when using a foo, and I want to handle all of them. Note that even without inheritance, Rust can have hierarchies of failure conditions through composition of enums, so I can handle Err(SomeErrorEnum::FooError(_)).

With type hierarchies, I can catch(FooError e) or rethrow if !(e instanceof FooError). But with integer codes I must rethrow if !is_foo_error(e) or e.code != FOO_ERROR_A && ... && e.code != FOO_ERROR_X. Either of those requires maintaining a list of all such error codes, and keeping that list in sync with the declarations of those error codes.

The latter is also fragile to changes in the list of possible error conditions. And it's particularly error-prone for novices, who often mistakenly write || instead of && when doing negative comparisons against multiple values. There is a Stack Overflow Q&A about that mistake with 275,000 views and 18 duplicates, so it's a very common pitfall, and worth steering people away from if you can.

Programmers won't bother to assign codes to their own errors

Numeric codes should be declared in one place, with a reference for what they all mean. This is extra work compared to just declaring a new class which extends Exception where you want to use it ─ with error codes you need a centralised place which declares them all.

Beyond that, you need to provide a set of helper functions so that when error 42 is thrown, the message accompanying it is the correct string. But many programmers will figure it's not worth all this effort, especially if they are already writing human-readable strings. That makes it harder to tell different error conditions apart programmatically later; all the places in the code which throw exceptions will do so in an ad hoc manner and need to be changed.

No checks to ensure correct construction of errors

If error conditions are represented by subclasses of Exception, then the constructors of those classes can check that exceptions are constructed properly with valid payloads. Probably the simplest way this is done is with access modifiers; an exception whose constructor is package-private cannot be incorrectly thrown by code outside of that package.

In contrast, if error conditions are represented by numeric codes, there's nothing to ensure that every throw-site uses these codes correctly, pairs the correct message and payload with each exception, and so on.

Numeric codes are less meaningful when debugging

If an exception makes its way to the top level of the program, then the program halts and prints the error details. An error code like 42 is less immediately useful than an exception type name like ArrayIndexOutOfBoundsException, because you have to go and look up what it means.

Numeric codes don't respect namespaces

Libraries written by different authors might use the same integer constants for different failure conditions. This wouldn't matter so much in C because each library would have its own functions for checking error conditions. But in your language, all libraries will throw the same type of exception, so error codes can become ambiguous at the catch site.

No custom payload

The homogeneous exceptions you propose don't have room for custom types of payload ─ only a byte array is allowed. While a byte array does allow passing serialisable types as the payload, it's inconvenient to serialise and deserialise data for this, and many types (e.g. anything holding a callback function) cannot be serialised. Alternatively, your language might allow byte arrays to be cast to and from arbitrary other types, but this completely sacrifices memory safety.

It's very useful, particularly when debugging, to attach arbitrary user-defined types to exceptions. When an object finds itself in an invalid state where its invariant has been violated, you want to throw an exception which passes that object up the call stack to the top of the program, so its state can be easily inspected. For example, in Javascript, the browser console can be used for this.

Custom payloads are also often useful for other reasons, e.g. when deciding how to handle an error condition, or when exceptions are the most convenient way to achieve a certain flow of control (e.g. here).

kaya3
  • 20k
  • 40
  • 120