Error Handling Patterns
andreabergia.com
andreabergia.com
fn doSomethingFallibly() -> SameVal | Error
SomeVal x = ...;
if failed:
return Error
else
return x
fn doTheThing() -> void
match doSomethingFallibly():
SomeVal x: /* use x */
Error: /* log the error */
To the utterly unreadable exception-based implementation: fn doSomethingFallibly() -> SameVal, except Error
SomeVal x = ...;
if failed:
raise Error
else
return x
fn doTheThing() -> void
try Someval x = doSomethingFallibly()
/* use x */
except Error: /* log the error */
This is especially egregious if you want to propagate the error. For the error variant case you can use a beautiful monadic solution fn doTheThing() -> void|Error
SomeVal x <- doSomethingFallibly()?
While the exception based variant completely obscures the control flow: fn doTheThing() -> void, raises Error
SomeVal x = try doSomethingFallibly()For errors that you can handle right in the calling function, this is ideal.
For errors that should kill your whole aapp, may be do some housekeeping (like abort the whole transaction that wraps your logic to reply to this http request) and get reported to sentry, catching exceptions at some highest level is far more usable.
I prefer to use both depending on meaning of the error.
Java already went in the right direction with distinguishing errors from exceptions and having "checked" (that you cannot ignore) exceptions, but the implementation of those concepts sucks. Also, generics, which allow the implementation of "sum types", came later and so sum types were never established as the way to do error handling.
This is my insurmountable objection to Exceptions.
What people will tell you is that you should only use Exceptions for "real" error conditions which should be rare, but the problem is that the people deciding whether to use Exceptions are writing some library, whereas the people who know whether this is a rare condition are the application writers, using that library only second or third hand, or maybe via some third party component. You might as well provide a language where the primitive integer types don't have a specified size so programmers don't know if numbers will fit in them...
... oh right yeah. That checks out.
- For the longest time, languages with typed (“checked”) exceptions lacked any parametric polymorphism worth a damn. That made the typing... less than optimally useful, especially with fallible callbacks. Sum types can borrow all that from the existing language, and you’d feel intensely awkward if you tried to go with untyped errors there (it’s of course still possible, through Dynamic or interface{} or whatever Rust’s thing is).
Though looking at the boilerplate fest that is the latest Rust (sum-types-based) error handling article posted here[1], I’m not entirely convinced the situation has improved. Perhaps open union types of some sort really are needed? That’s where the algebraic-effects languages seem to be going, anyway.
- Exceptions are more strict about what you can do with them: you can return or you can bind, monadically speaking. If an exception was raised, everything stops until it’s caught. With sum types (concrete ones, not an abstract error monad you can’t run), you are allowed to have several propagation chains at once and manipulate them as data. The lack of that additional flexibility is what makes exceptions (more cheaply?) implementable in terms of stack unwinding.
(Traditionally, stack unwinding is thought of as expensive, but I don’t really see why. Sure SJLJ-induced register spills are bad, and sure table-based unwinding is slow as hell, but a linked list of frames as in Win32 SEH[2] has to be cheaper than the same thing implemented as conditional jumps in every unwinding path, right? Right? I must be missing something here, expired Borland patent aside.)
- Concrete sum types cannot implement resumable exceptions[3], as far as I can tell. I’m almost sure an abstract error monad could, but for some reason I have never seen it done. Besides, working with that is going to require more syntactic support than what you’ve shown. (Yes, I know, such syntactic support is possible to implement and has in fact been implemented.)
[1] https://sabrinajewson.org/blog/errors, discussed at https://news.ycombinator.com/item?id=35502874
[2] http://bytepointer.com/resources/pietrek_crash_course_depths...
[3] https://gigamonkeys.com/book/beyond-exception-handling-condi...
2) The next step up is making the exception continuation first class, then I don't think there is any difference. I think you can recover the efficiency of the simple non-local goto by optimizing after CPS transform, but I'm not a compiler writer.
3) As you point out, I think the continuation monad should be able to do resumable exceptions.
You mention algebraic effects, which are probably the right way to implement Error types/Exceptions/Resumable Error handlers.
fn doTheThing() -> void, raises Error SomeVal x = try doSomethingFallibly()
Functions don't do things, they process and return things. Side effects are handled higher up at the root of a program. Generally any time you'd be "doing a thing" you'd also be in a position to handle the error.
source: I don't do any functional programming but have seen some videos on youtube
fn doSomethingFallibly() -> SameVal, except Error
SomeVal x = ...;
if failed:
raise Error
else
return x
fn doTheThing() -> void
try Someval x = doSomethingFallibly()
/* use x */
except Error: /* log the error */
To the utterly unreadable error variant based implementation that uses obscure functional stuff like patter matching: fn doSomethingFallibly() -> SameVal | Error
SomeVal x = ...;
if failed:
return Error
else
return x
fn doTheThing() -> void
match doSomethingFallibly():
SomeVal x: /* use x */
Error: /* log the error */
This is especially egregious if you want to propagate the error. The exception based variant can focus on the success control flow: fn doTheThing() -> void, raises Error
SomeVal x = try doSomethingFallibly()
While for the error variant case you have to use even more obscure monadic code: fn doTheThing() -> void|Error
SomeVal x <- doSomethingFallibly()? for
x <- fn1()
y <- fn2(x)
z <- fn3(y)
yield z
That is both readable and safe, here fn1-fn3 all return an Either[Error,T] (the error type has to be common but the success type can vary by function).Each of the three major paradigms (return values, exceptions, error handlers) has pros and cons, neither is strictly superior.
BTW, Error is winning 7-6 at the moment :)
That way, if you ever write code that doesn't error-check before trying to get the "happy path" result, you'll notice immediately, even if it's the kind of thing that very rarely fails and you haven't written a unit test for it.
A chaotic good CS educator might want to try out an experiment: give the students an assignment where they have to work with an API that uses a normal result type, explain why error-checking is really important, but when you're marking the assignment switch out the result type for the Kroll one.
Rachel seems fixated on people learning a lesson from using it wrong, rather than shifting hard left like Rust's Result so that when you get this wrong your code doesn't compile the same as if you forget to quote strings, or you write the actual arithmetic product symbol instead of an asterisk. There is no need for this code to compile in order to "learn" something.
That's the same lesson which scraped into C++ 20 errata, to make std::format("{} {}", 5); into a compile error whereas previously it was ludicrously a runtime error. Maybe at runtime the literal 5 will be two values ? No. We don't need to execute this code to realise that it's wrong. Shift hard left.
I'm guessing Rachel's decision was not so much fixation and more that only the standards body (or the designer of a new programming language) could make something into a compile-time error, but a SWE/SRE can take a small step like this on her own.
Both force you to handle the error or explicitly ignore it, which is the correct way. Either you take care of it there if you have the context to figure out if it's relevant or not or you explicitly bump it one step up for the caller to handle.
The only issue I can think of comes from ignoring the error. You get the same problem with Rust's Result btw. Static analysis helps in this regard, golangci-lint catches easily such mistakes.
In my experience, with production tooling, I never encountered what you described. Though, from a pure aesthetic point of view I prefer Rust's Result.
1. T is an int that's sometimes legitimately zero, if you return (0, err) and the caller isn't careful they can look at the 0 without realising it's not valid.
2. T is a 'pointer' type so you return (null, error) and the caller tries to dereference the pointer anyway (one example I've seen is inserting a logging statement for debugging purposes just _before_ the error-check, along the style of `logger.debug("foo {t.name}")` where the braces interpolate stuff).
3. T lives in a strongly typed world where there's no convenient null value available (maybe your language distiniguishes between "definitely a T" and "either a T or null") so you somehow have to construct an instance of a T even though you're never going to use it and it'll most likely be invalid in some sense.
That doesn't mean the golang approach is bad - especially not in golang itself - but you do need to know how to use it correctly. Then again, using monadic error handling in golang would probably be strictly inferior - it's not that you can't define one, but the language doesn't have the syntactic sugar to handle it like e.g. Scala does, and I find that being able to use that sugar to get some readability for the approach is the whole point of monadic error handling.
Caller: checks the error, which is not fatal, but expects to get a nil in this case. Ends up with a non-nil partially initialized garbage.
Yes, someone screwed up. Yes, it's a bug. Doesn't change the fact that there are better error handling approaches that eliminate this kind of bugs completely
One easy way to prevent these sorts of errors is to always return default literals with errors (in the same spirit of "value == var" from c to prevent accidental assignment).
Implicit zero values are a bad design decision that cause all sorts of other pain. Consider the behavior of sends and receives on nil channels [1]. This is a fundamentally nonsense operation, but Go's designers are forced to define a behavior because nil channels can exist thanks to implicit zero values.
How? Take the easy example, "clowns" is a string that's supposed to be an small integer. How do I get this wrong in Rust? We have to say what we want to happen when it won't parse which isn't ignoring it.
let clowns: u16 = clowns.parse(); // Won't compile
If we say we believe this won't happen we can express that, but when it does happen the program panics... let clowns: u16 = clowns.parse().unwrap(); // Panics if clowns doesn't parse as a u16
We can say what we actually want to happen, but how is that the same problem? let clowns: u16 = clowns.parse().unwrap_or(1629); // OK it's 1629 if it won't parse
Finally we can write the C-style "Nothing will go wrong" attitude using unsafe, but I don't see that as a problem, if your first instinct is to reach for unsafe Rust you're a bad programmer let clowns: u16 = unsafe { clowns.parse.unwrap_unchecked() }; // Mark Baum says "Boom". a, err := strconv.Atoi("blah")
b, err := strconv.Atoi("123")
if err != nil {
log.Fatal(err)
}
return a + b
This could be fixed to emit a vet error, but ultimately it is inferior to only returning either error or success, instead of returning both even through logically only one exists.[1] https://github.com/gordonklaus/ineffassign [2] https://golangci-lint.run/
That is definitely not true of the second, especially when the language is incorrectly or insufficiently pedantic like Go. Notably, because the base pattern is
foo, err := Bar()
it is very easy to write something like: foo, err := Bar()
// code, no error check
bar, err := Baz()
// code, no error check
qux, err := Qux()
if err != nil {
...
}
And Go is perfectly happy with that, because while it will complain about unused variables it doesn't say anything about dead stores. You need external tools to warn about this issue, as well as ignoring the returns entirely.And then of course because this pattern requires having a valid "result" value which ends up in scope, it's very easy for bugs to creep in where this result value does get used even in case of error.
It's much more common to see
if foo, err := Bar(); err != nil {
It's a pretty immediate yellow flag for me if you're not doing that.In general, I've found that not catching errors is not a practical problem when writing go. Whereas things like interface nil ambiguity and the way defer and closures interact bit me a few times.
package main
import (
"fmt"
)
func Bar() (string, error) {
return "hello", nil
}
func main() {
if foo, err := Bar(); err != nil {
panic(err)
}
fmt.Printf("foo is %q", foo)
}
$ go run foo.go
./foo.go:12:8: foo declared and not used
./foo.go:15:30: undefined: foo
> In general, I've found that not catching errors is not a practical problem when writing goYeah, good for you, but I am obviously not a consumer of your code because I have lost track of the number of times I have to use software written by folks that try to be heroes and program using dumb editors that cheerfully don't warn about swallowing err
It's also severely missing in breadth e.g. Swift and Zig use an exception-type syntax, but result-type semantics, it does not cover conditions and restarts, or takes like Common Lisp's MRV, where the ancillary return values are treated as secondary and interacted with through special functions.
To be fair, it is very similar to a `Result<T, E>` type, which is why I made this library a while ago: https://github.com/linkdd/rustic_result
It's basically the same thing, in a language with pattern matching but not static typing. In Erlang/Elixir it's conventional to use tagged tuples to emulate sum types.
Go seems to be the only one holding up the ground of caring about cognitive load on developers and code readability.
* errors from async-colored functions (ie. .then/.catch/.finally and higher order combinators on them, ie. we use a lot of p.catch(log.rescue(...)))
* errors in generators
* errors in async generators
Using functional approaches helps working with things like managing severity, error codes, nested errors etc. that may be attached to errors as well as managing higher level concepts like timeouts, retries etc.
This article reads like hello world for errors, there is more to it.
Okay, I add
if (printf(...) < 0) {
// TODO: Handle error
}
around the printf call. Now what? How do I handle the error? In most realistic scenarios I can imagine I'd either just ignore it and keep going, or print an error and abort (but what if printing an error fails too? Oh no...), or I'll never actually even get the chance to handle the error because my program will get killed by a SIGPIPE.Seriously, if the printf fails then (barring the malformed format string) that means that the underlying device lost its ability to output data and this ability is most likely not coming back, and your program generally can't do anything with it, or even know about something to do: the functions from the FILE*-family abstract the underlying devices extremely well.
I can think of several other reasons:
- temporary glitch
- multi-byte character conversion fails because the bytes sent and the current locale don’t match
- printf runs out of memory
There probably are more.
Ideally, that properly should be handled in the device driver, with buffering and retries, and not bubble all the way up. But even if it does, you can't reasonably retry the printf call since there is no guarantee that zero data has actually been written.
> multi-byte character conversion fails because the bytes sent and the current locale don’t match
That falls under "malformed format string" category; using printf for MBCS-conversion is almost always a programmer's error.
> printf runs out of memory
That falls under both "stupid library implementation" (printf doesn't need to malloc) and "still can't recover from that".
That’s changing the subject. The comment I replied to claimed “the underlying device lost its ability to output data”, not that retrying wasn’t an option. My point is that the call can fail even though the device is fine.
> printf doesn't need to malloc
The standard allows it to do so, and https://stackoverflow.com/a/6743056 claims glibc did that in 2011. Looking at https://github.com/lattera/glibc/blob/895ef79e04a953cac14938... and https://github.com/lattera/glibc/blob/895ef79e04a953cac14938..., it seems vfprintf still can do that today, but I didn’t check whether printf can call that.
> and "still can't recover from that".
See above; I never claimed that. Also, this action (in general) isn’t recoverable, but programs sometimes can recover from out of memory conditions, for example by clearing their caches or by freeing a block of memory specially allocated at startup to ensure some recovery from an out of memory condition is possible.
Mmm. Now that I re-read my original comment, it does look like this. But my main point that I wanted to make was "since error from printf() is (almost always) unrecoverable, there is no much point in checking for it, you can't really do anything useful with it, so why do posts that chastise people for not checking it keep getting written?"
And the main reason for a printf() error, in my experience, is indeed the "broken" device which is usually a network socket/pipe or, rarer, a file on a network share or, even rarer, a file on a disk that ran out of free space. That's not something a program can (or even should try to) work around of, that's what its environment should do.
As for the fact that glibc has malloc()-ing implementation of printf() — glibc has many strange implementation decisions. For example, its sscanf() interanlly calls strlen() on the string to scan, so calling e.g. sscanf(1GB_LARGE_BUFFER, "%d", &out) is a bad idea, especially in a loop: https://news.ycombinator.com/item?id=26300451
In the case of opening a file, you would get an invalid file handle that you can still pass around to functions and nothing would "fail"; it would just be as if you passed a handle to /dev/null. This scheme requires that you can still explicitly check if the handle is valid.
Most people are familir with some form of this with the special NaN floating point value.
Therefore, I'd be very wary of using this pattern elsewhere, unless you can carry along a backtrace to help track down the culprit.
On the other hand, it is useful that GL shaders will still sorta-kinda work even if you have zero divides, rather than completely blowing up. In many cases this only happens on a single pixel with very specific inputs, and you can just ignore it -- like having a single dead pixel on a monitor.
Edit to add: like, if the error is "distance was zero so I couldn't invert the matrix", I want that to be the error feedback; not "the screen is black because the final result of all the camera calculations was NaN".
Objective-C has this, where errors would usually end up returning a `nil` (with an NSError accessible via an optional out parameter), and sending a message to `nil` has no effect and returns `nil`, so you could code on the happy path and end up with no idea whatsoever why your thing stops working, with the only information being that you end up with a `nil`.
libc error handling also kinda but kinda not uses this pattern, with the additional fun that you can UB on the way: libc does not normally reset errno, so you can reset it (to 0), call a bunch of functions, and check errno at the end.
Except that doesn't really work because:
1. not all functions will signal errors via errno
2. third-party code may be resetting errno for its own use (errno is not unlike a caller-saved register in that sense)
3. third party code may handle errno internally, but will not reset it if so, leading to sprurious error messages.
Shameless Plug: "Musings about error handling mechanisms in programming languages"
https://www.amazingcto.com/best-way-to-handle-errors-for-a-p...
-ignore them -squelch them ("this could never happen")
Ackshually, ...