Skip to main content
added 83 characters in body
Source Link
kaya3
  • 20k
  • 40
  • 120

You say that Rust does error-handling without inheritance, but inheritance is a red herring here. Your proposalhere; what matters is to have just one exception typeallowing custom types, whereaswhich Rust does and your proposal does not. Inheritance is how languages like Java allow multiple exception types via inheritance. But Rust allows multiple error types viadoes this through 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 needRust's use of composition instead of inheritance for this does mean that values representing errors to have a common supertype, because it doesn't automatically generatedon't get stack traces "for free" ─ if you want a stack trace when, you construct an Err(e)have to build it in your own constructor, and include it as a component of your custom error type. In languages like Java where stack traces are guaranteedautomatic, 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 codeSo Rust loses something here from not supporting inheritance, but it's not bothheterogeneous error types.

In contrast to both Rust and Java, 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:

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. For example, in C if you call mysql_errno() then you know the code returned represents a MySQL error. But in your language, all libraries will throw the same type of exception, so error codes can become ambiguous at the catch site.

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:

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.

You say that Rust does error-handling without inheritance, but inheritance is a red herring here; what matters is allowing custom types, which Rust does and your proposal does not. Inheritance is how languages like Java allow multiple exception types. But Rust does this through composition instead; Result<T, E> allows any type to represent a failure condition, including enums, which are easy to filter with match.

Rust's use of composition instead of inheritance for this does mean that values representing errors don't get stack traces "for free" ─ if you want a stack trace, you have to build it in your own constructor, and include it as a component of your custom error type. In languages like Java where stack traces are automatic, exceptions must inherit from Throwable, so that they invoke its constructor which builds that stack trace. So Rust loses something here from not supporting inheritance, but it's not heterogeneous error types.

In contrast to both Rust and Java, 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:

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. For example, in C if you call mysql_errno() then you know the code returned represents a MySQL error. But in your language, all libraries will throw the same type of exception, so error codes can become ambiguous at the catch site.

Source Link
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).