The two kinds of error
13 points by EvanHahn
13 points by EvanHahn
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.
Strong agree. Burntsushi wrote a much more detailed analysis of this error distinction: https://burntsushi.net/unwrap/
I’m sorry to say this, but they missed “off by one” errors.
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
I think errors are a bit more nuanced. Years ago I came up with four types of errors (near the bottom of the post) that I'll summarize here:
EBADF (not an open file, or the operation couldn't be done given how the file was open originally) or EINVAL (invalid parameter) that need to be fixed, but once fixed, never happens again;EACCESS (bad privileges) or ELOOP (too many symbolic links when trying to resolve a filename) but that the fix has to happen outside the scope of the program, but once fixed, tends not happen again unless someone made a mistake;ENOMEM (insuffient kernel memory) just happened and things are going bad quickly. Depending upon the circumstances, a fast, hard crash might be the best thing to do;ENOENT (file doesn't exist) depending upon the context (it could then create the file, or ask the user for a different file, etc.).Type 1 are bugs; type 2 and 3 are operational type errors, fixable, but outside the program (with type 3 being resource exhaustion and are typically hard to deal with, and could double as a type 1 error (memory leak for example)); and type 4 are situations that the program can handle in the normal course of running. Yes, types 1 to 3 can be classified as "unexpected" and 4 as "expected" but that's a bit too course grained for my liking.
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.
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