Error ABI
A follow-up on the “strongly typed error codes” article.
One common argument about using algebraic data types for errors is that:
Error information is only filled in when an error occurs, And errors happen rarely, on the cold path, Therefore, filling in the diagnostic information is essentially free, a zero cost abstraction.
This argument is not entirely correct. Naively composing errors out of ADTs does pessimize the happy path. Error objects recursively composed out of enums tend to be big, which inflates size_of> , which pushes functions throughout the call stack to “return large structs through memory” ABI. Error virality is key here — just a single large error on however rare code path leads to worse code everywhere.
That is the reason why mature error handling libraries hide the error behind a thin pointer, approached pioneered in Rust by failure and deployed across the ecosystem in anyhow . But this requires global allocator, which is also not entirely zero cost.
Choices How would you even return a result? The default option is to treat -> Result as any other user-defined data type: goes to registers if small, goes to the stack memory if large. As described above, this is suboptimal, as it spills small hot values to memory because of large cold errors. A smarter way to do this is to say that the ABI of -> Result is exactly the same as T , except that a single register is reserved for E (this requires the errors to be register-sized). On architectures with status flags, one can even signal a presence of error via, e.g., the overflow flag. Finally, another option is to say that -> Result behaves exactly as -> T ABI-wise, no error affordances whatsoever. Instead, when returning an error, rather than jumping to the return address, we look it up in the side table to find a corresponding error recovery address, and jump to that. Stack unwinding! The bold claim is that unwinding is the optimal thing to do! I don’t know of a good set of reproducible benchmarks, but I find these two sources believable: https://joeduffyblog.com/2015/12/19/safe-native-code/#error-model
https://youtu.be/LorcxyJ9zr4?si=HESn1LfHek5Qlfi0 As with async, keep visible programming model and internal implementation details separate! Result can be implemented via stack unwinding, and exceptions can be implemented via checking the return value.