The two kinds of error

13 points by EvanHahn


snej

I agree overall, except for when you bring in the throwing-vs-returning controversy, which is unrelated. Those are two ways of reporting errors, which are orthogonal to the type of the error.

(The third way to report an error is simply to abort the process, something only used with unexpected errors and usually only in systems languages.)

Some programming languages, like Rust and Zig, classify many errors as expected. Others, like JavaScript and Python, classify them as unexpected. For example, when you parse JSON in Go, the compiler makes you handle the error; not so in Ruby.

You’re conflating the type of error with the way it’s reported. Parse errors from a JSON parser are always expected. Some languages/APIs throw them, others return them. Go happens to be a language that uses throwing only for unexpected errors (I think Rust too?) Ruby is one of many languages that almost always use throw (others are C++, Java, Python, JS, Kotlin, …)

(I also disagree with “the compiler makes you handle the error.” It’s very easy to ignore a return value, and C code is full of bugs like that; much harder to ignore a thrown exception with a try and an empty catch. Simply not catching the exception isn’t ignoring it as passing responsibility to the caller, which is what the correct thing most of the time.)

Then Swift throws a curve-ball by using throw/catch syntax but as syntactic sugar for returning a Result type. There have been proposals to make C++ do that too.

justinpombrio

Strong agree. Burntsushi wrote a much more detailed analysis of this error distinction: https://burntsushi.net/unwrap/

doctor_eval

I’m sorry to say this, but they missed “off by one” errors.

oftenwrong

This is a pretty good write up on errors in general, and I think it makes the same distinction with different terms. It divides them into "'operational errors" and "programmer errors".

https://console.tritondatacenter.com/node-js/production/design/errors

rtpg

The thing that kinda gets me is whenever "allowed to crash" interplays with higher level systems that will try to recover from crashing.

I had this for a while with Firefox. In the font/styling code (written in Rust) there was a panic! just deep in some code. But it wouldn't pull down all of Firefox. Instead, FF could recover from it... but I would end up with a weird corrupted font map and suddenly I'm seeing Tofu blocks everywhere instead of Japanese.

I do legit believe if that panic! had been transformed into a Result (especially ironic cuz it was in a Result-returning bit of code) then there likely would have been smoother handling downstream that could kick in.

Python-style "exception" crashes where the outer loop recovers anyways is kinda fine for Python-ish code, but for systems code you really get into the problem of invariants no longer being valid. So if your outer loop does recover from inner loop failures, it seems hard to say what, if any, crashes are allowed.

mdaniel

For example, when you parse JSON in Go, the compiler makes you handle the error

splitting hairs, but that's only true if the err is a sibling part of the return signature that otherwise contains something you want; otherwise golang cheerfully allows eating any returned values, and (regrettably!!!!!) does not treat error in any unique way

package main
import (
	"encoding/json"
	"fmt"
	"strings"
)
func main() {
	var foo int64
	dec := json.NewDecoder(strings.NewReader(`"hello world"`))
	dec.Decode(&foo)
	fmt.Printf("ok, %+v\n", foo)
}

likely what OP was thinking of is signatures such as func Awesome() (int64, error) which would require either assigning error or voluntarily masking it with _ in order to access the int64 part