Errors – Gotta Catch 'Em All
blog.twitter.com
blog.twitter.com
Shameless plug for my open source PHP error handler: https://github.com/slashtrace/slashtrace. I use it as a catch-all mechanism that displays detailed information about the error when running in a development environment, but sends the data to an external service (Sentry in my case) when in production.
Obviously there are a ton of places in the code where you want to catch and handle exceptions yourself, but having a catch-all acts as a safety net.
I do like their approach of logging stats for exceptions that don't represent system errors, but which they monitor anyway (like the BadClientRequestException).
I disagree with this, on a very strong and deep sense. When you program, there are no exceptional cases. All possible behaviors should be treated on equal footing, using the control structures of your language. When you try to open a file, either you can or you cannot, and none of these two cases is exceptional in any way.
I would rather say that a programming language that supports exceptions is broken by design. If the language even forces you to use the exceptions, then it leads inevitably to spaghetti code.
That's because you're inexperienced.
> All possible behaviors should be treated on equal footing, using the control structures of your language.
That's impossible if you're dealing with a system composed of multiple interacting abstraction layers.
An error inside a function precondition is not the same as an error crashing a thread of execution, which in turn is not the same as an error in logical consistency communicating between different services.
The "equal footing" you're talking about can only exist if you're dealing with very simple and linear programs.
That's fairly rude to the guy, and doesn't make your point any stronger.
I've worked with exceptions before, it leads to try/catch spaghetti code.
You can't possibly know instantly all the exceptions to handle for a function (given it calls other functions w/ exceptions), whereas, if it returns a fixed bunch of error codes, then you can know why that specific function failed and handle appropriately.
Doesn't help that (language) exceptions typically have awful codegen.
There's a reason nobody uses them in game-dev.
> You can't possibly know instantly all the exceptions to handle for a function (given it calls other functions w/ exceptions), whereas, if it returns a fixed bunch of error codes, then you can know why that specific function failed and handle appropriately.
presume that, despite not being able to know all exceptions to handle in advance, you somehow have knowledge of all error conditions in the event that you're using error codes?
I guess that it is indeed the case. If you call one function, you can read the documentation of said function and read about its return values, including error codes. This specification cannot not change if one of the libraries nested deep within this function is replaced. However, with exceptions, if you use a new implementation of a deeply nested function it may raise a new exception that nobody was aware of at the time of writing the outermost functions.
You also can't possibly know all error codes to handle for a function if it calls other functions returning error codes, unless a type system enforces that for you. In which case you should compare to a language where the type system enforces that all exceptions are handled.
You aren't using error codes or documenting correctly. You get a choice whether the error propogates upstream
Example: you have a method which requests a file from disk, but it fails to load for some reason. With error codes, you force all callers of your resource loader to explicitly handle the failure case at the point of failure, or you lose all context on the error. If you just bubble the error up, you're manually doing exceptions.
With exceptions, the code that requests a file can assume it succeeds if it doesn't have enough context to handle the erorr, and you can handle at a higher level and still know what happened.
And you never should. That indeed is horrible design.
What you do need is a separation between 'exceptions that cause hard failure' v.s. 'exceptions that can lead to soft restarts'.
You can easily separate them using common sense just looking at where your interface is located.
The main advantage (just picking reasons off the top of my head) is that you can write your code as if it will certainly succeed, then have a separate block of code for handling failure cases. This makes code a lot more legible and draws explicit attention to failure case handling. Some languages make a distinction between 'checked' and 'unchecked' exceptions (e.g. Java) whereby checked exceptions must be handled at the call-site, whereas unchecked exceptions can be caught anywhere in the call stack (or not at all, in which case the thread or program will terminate). It is often conventional to forego checked exceptions and only use unchecked exceptions to provide flexibility to the caller in where they handle the exception. Some languages such as Kotlin remove the concept of checked exceptions from the language entirely.
Languages like C which don't have exception handling force the programmer to use return code checking to verify if a method call has succeeded. This is fine, but as I alluded to above, can add noise to code and diminishes flexibility.
One example of the power of unchecked exceptions is e.g. an exception mapper in a REST server. This allows the programmer to throw an unchecked exception at any point in the server application code and this will (if not handled by the application code explicitly) get caught by the exception mapper. It will then map this exception to an appropriate HTTP response code and return it to the API user.
An argument against exceptions is that there is a large 'overhead' to handling them. I've yet to see a scenario where an application performance bottleneck is being caused by exceptions. YMMV.
Yet there's millions of google hits for "exceptions should be exceptional" and variations of it. This meme has taken hold in a big way. I've tried to use them in other ways -- after all, as the author of a program, how can I possibly know which branch will be the common one, for the user? -- and I always get beaten down by other developers with this aphorism.
As this article points out: "The naming of ‘Exception’ is actually quite unfortunate."
In the return code case, we're using if/then to handle error situations.
In the exception code, we're using try/catch.
With respect to spaghetti, they're literally the same thing, just differently indented...
edit: or the PHP special of hitting an error down the stack and again just dying without passing the message up the chain. That is no way to work.
Oh, absolutely. That's how I do it too. Unfortunately, warnings and notices are still a thing in PHP 7. So the best practice is to just install an error handler that turns these into exceptions. They they are either handled by code, or by a catch-all handler.
This is in fact what I've implemented SlashTrace to do by default: https://github.com/slashtrace/slashtrace/blob/master/src/Err...
Error codes and exceptions are both awful for control flow. Typed errors and checked exceptions work better for known errors but break down for IO because you can't handle all IO errors - new OS versions can add new ones. And all of these are awkward when propagating errors between threads.
Okay, so we'll return early with an error code and probably some kind of nil result. (Because we're not using a system that's so archaic that it can only return one value!)
But the caller of the library was some middleware that also can't be expected to handle the error usefully. It returns the same error and result to its own caller. And let's say there are a few layers like this until we finally get to the top layer where we can translate that error code into a big, red message for the user saying "file not found".
Now how is that any different from raising an exception? (Other than the extra 'if error: return error, result' lines you need.)
I think exceptions can be used well in some contexts but I will completely concede the point that they're super complicated.
"How is that any different?" Well -- we unwound the stack automagically and that's not trivial. Every intervening piece of code should know that a call they make could transitively make lots of interesting calls and they might cause this function we're writing to exit early.
Exception bubbling is implicit.
There's no way to know which exceptions can be thrown from a function just by looking at its code, nor looking at the called-functions code. You have to keep in your head all the call chain up to the first function that does not call any other function (good luck with that).
Even worse: we don't even know _where_ the function can stop running and start unwinding. This leads to defensive programming. Hello try-catch soup!
It's a slippery slope that gets worse and worse with each abstraction layer.
Languages with typed errors have been nothing but a joy for me.
But is this different from error codes? Does seeing a file open tell you every error that might occur from that?
If you're arguing that you could just look at the file-open code to see the errors it returns, couldn't the same apply for seeing which exceptions are thrown?
No, because then you have to look at every function inside there. And there every function inside those functions. And then...
With typed error handling they're only one-level of abstraction deep. Actually zero-levels, since your function won't typecheck unless you handle the errors.
Mostly yes, usually as a sum type.
Pattern matching is your friend.
> the errors are massaged
Why would I take the extra effort?
Unless it's my responsibility to handle it (i.e. it's part of the function's logic) I will just return it untouched and let the consumer decide what to do (if anything at all).
> or placed untyped
That's not possible (at least in the typed languages I tried).
Making the real error types part of the signature of the method means that implementation changes require changing the signature, completely breaking encapsulation. In fact, I'd argue it makes polymorphism completely impossible.
Yeah, well, Java's semantics and programer laziness make a mess. News at 11.
> I'd argue it makes polymorphism completely impossible.
Oh, so two birds with one stone!
I don't see why.
Putting error conditions in the function signature means that the signature reflects the implementation details of the function. Change those implementation details and the signature changes, affecting the callers and their signatures. Unless you abstract the error and then you lose the only advantage of passing the errors around rather than using exceptions.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Let's add a new error condition:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Only the function raising the error, and the consumer, had to change (and the consumer only changed because it _cared_ about the error type!)
If you want an example with nested errors I can do it, you'll see it's mostly the same.
This isn't a realistic example at all because your MyError is simple enum that you changed to make it work. If you add another error, or have more than one level, or call it multiple locations, and don't eat the error then now you're changing many more functions, just as I said.
Well, that's what an error is. A simple enum of the error cases and maybe some metadata.
EDIT: e.g. IO errors in Rust's std: https://doc.rust-lang.org/src/std/io/error.rs.html#91-174
If your first guess is this isn't realistic because it's "too simple" then maybe you've never been exposed to this kind of error handling? It's so simple it's unbelievable?
> If you add another error
That's what I did, I added a new error.
> or have more than one level and don't eat the error now you're changing more methods, just as I said.
As I said, if you want an example with nested error types I can do it:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
As you can see, caller() (which would be our "middleman" here, the one that neither produces nor uses the error) didn't change at all.
> If you had an external compiled library the consumed MyError or called caller, you just broke it, becuase you changed the type.
You mean a dynamically-linked library? Yep, completely broken, but that's not the only problem with DLLs. Static linking is fine.
Anyways, the contract changed, so it's a breaking change, so that's good in my book. The contract changed in the case of exceptions too, you just hand-waved over the issue and called it a day.
Wouldn't it be the same with exceptions across library boundaries?
I explained why the example is too simple. If you coded this in C with an integer error code the code would also be roughly the same size.
> Wouldn't it be the same with exceptions across library boundaries?
Actually no. That's the significant difference.
I can freely change the implementation and if that causes new and different exceptions to be possible, that doesn't affect the implementors. And most of the time such a change makes will make material affect on the implementors so they are broken for nothing.
And I offered a second example with a nested error, and nothing changed. Errors are just enums, there's nothing more complex than this. It's that simple.
> If you coded this in C with an integer error code the code would also be roughly the same size.
But it wouldn't check the error types. It's a zero-cost abstraction.
> I can freely change the implementation and if that causes new and different exceptions to be possible, that doesn't affect the implementors.
Neither does in my case... unless I care about the actual errors, which I did in this case in main() just for completeness.
What value is the enum value Other(OtherError) actually provide other than to satisfy the compiler? How far do we take this when there are more levels? OtherFailure(OtherMistake(Other(OtherError)))?
> Neither does in my case...
In each example you changed the implementation of type so that it is no longer compatible. You altered the signature of the method.
> But it wouldn't check the error types. It's a zero-cost abstraction.
It's funny that you say that as error returns have a non-zero runtime cost. Yet exceptions can actually be zero-cost at runtime (if they aren't thrown) as they can be implemented out of band. Only the most naive implementation propagates exceptions with multi-value returns.
Now I do understand why Rust is implemented this way but it's not because error returns are unequivocally better.
Good code with exceptions have very little code actually related to error handling. Throw often, catch infrequently, and use normal resource cleanup for exceptional cleanup as well. You can get far with a 2-3 well placed catch statements and that's it. No extra typing. No error propagation code.
If you think of error handling as happening where the error is raised as opposed to where it is handled it's hard to see the advantage of exceptions. Where the error is raised is not that important.
What's a language that does exceptions just right for you?
If I had to choose between checked exceptions and well typed error propagation, I'd choose the latter hands down. Checked exceptions have all the disadvantages of error returns and none of the advantages.
This meshes with my point about where the error concerns are; Java requires you to be very concerned about what errors are thrown at every method definition and call. But C#, which is a very similar language, doesn't. In C#, you only have to be concerned about where and what errors you can handle.
It's good you don't actually need to know that! And if you did, it wouldn't matter anyway because somebody could come along in the future and change that code that currently uses a database and change it a web service and all that knowledge would be wasted.
All you need to know is what exceptions you can handle and where you handle them. What error codes or exceptions each individual function everywhere up the call chain can raise is almost inconsequential.
Ah, so that's "all" I need to know!
And that's what typed errors do.
Do I need to check the return type of the thousands of methods that might make up that operation? Nope. Typed errors make me do a lot of work passing stuff around and declaring types that literally don't matter. If somebody adds a new method that does another network call, I'm still good and I didn't have to do anything.
I don't think we can discuss deeper than that.
The other thing I don't like is pointless boilerplate code.
But I do like strongly-typed languages and originally I thought I'd like checked exceptions before realizing just how wrong that concept is. It's not hard to see the parallels between checked exceptions and error returns.
My choice of language wouldn't have the concept of "null".
If it somehow returns an Option type... well, the docs would say it, and it won't typecheck unless I handle it.
> If you want strongly typed errors then you could use a language with checked exceptions (Java).
Yes, typed error handling looks an awful lot like Java's checked exceptions.
Buuuuut exceptions magically unwind the stack at unknown points. I don't like my stacks unwinded unless I explicitly tell them to. Function exit points should be explicit.
Also: it has unchecked exceptions too, runtime exceptions (null pointers, anyone?) and lacks the expressiveness to handle error propagation while not being a pain in the ass.
Disagree, in my favorite language you just add "?" to the possibly-erroring function et voilá, error propagated. Even better, since errors are functor-like (map) and monad-like (and_then), I'm mostly never unwrapping myself.
This has the advantage that it's explicit, instead of exceptions implicitly returning early (sometimes to great amusement).
And one of the first things I had to learn in rust was how make this happen with different error types. After that it was pretty much the same as exceptions. What functions return errors is a little more obvious, at the cost of extra boiler plate (most stuff being Result<...> anyway)
Checked exceptions Java style could be a perfect balance with just a few modifications. Compiler/IDE should mostly know whats thrown where, so specifying it manually should rarely be necessary. And still could visualize what can fail and with what errors.
Invariably you will have error conditions; situations that are expected work in order for your application to function. Your application is expected to open a file to work, if it can't open the file then that's an error. Your program cannot continue to do whatever it was supposed to do.
You make a call and it can't do it's job. So it returns to you an error and maybe that means that you can't do your job now. So you return an error to your caller. And if you do that all perfectly, you're basically doing exception handling without any help from the language.
Instead exceptions have become every day errors. Like the article defines,
> The handling of exceptional cases; perhaps a service failed for an unexpected reason like a network failure
Really? A service failure or network failure is an exceptional case for you? This should be planned for, and built into the architecture! Plan for the network always failing! Recover gracefully! That is what retry strategies like exponential backoff are for, and circuit breakers, idempotent requests so you can simply cancel and try again, so on and so forth.
Instead of treating everything as exceptional, when you treat errors as values, they lend you down the path of actually handling them. As a side effect of this, you build more robust systems. Bubbling up an error to the top when errors are values are seen as an anti-pattern, only to be done in the most rare of circumstances.
Again, from the article,
> All other exceptions - Signifies my server has a bug
Then your server should crash! Take it out of the load balancer, stop the damage! If you truly can't classify it, it's truly unknown or exceptional, abort!
So no, nobody is considering a network failure an exceptional case.
What if all the servers have the same bug? Should they all crash or should they alert and allow an operator to chose how to handle? If it is a rarely hit error case, staying up is better than creating a complete outage.
Exception do let you plan for exactly that while ignoring the ridiculous notion that you can know every possible error that every function can trigger now or in the future.
Instead you plan for exactly where you can restart a network operation in the code and put your exception handler in that spot. You don't need to do anything else. It's simple and effective.
The alternative is to write a lot of boilerplate code that does nothing but get you to that same point.
If you have a statically typed language, and defined error types that are only thrown in certain situations, can you please explain to me why it is so ridiculous to think you cannot know every possible error that will occur? If function a calls function b, and function b throws errors x y and z, why is it I cannot check for x y and z in function a? Additionally, say there are some additional outside runtime errors that can occur such as out of memory errors, again these are ideally known ahead of time, and you can use a constrained runtime to minimize them. Taking this a little further, if you cannot know every possible error, then you cannot possibly have any sort of remote closeness to formal verification, and that means our software systems that rely on more stringent verification methods are a lie. (I hope you don't write any medical device software!)
I am making an assumption and maybe think you are just trying to say this heuristically ? Like maybe we are all writing web software, and meh, close enough if you handle most errors, and "meh" for the rest of them? I mean I certainly think that works for the majority of development that happens, but more and more we are becoming keenly aware that our software has real world impact on people's lives and maybe some added stringency is what we need.
> If function a calls function b, and function b throws errors x y and z, why is it I cannot check for x y and z in function a?
How often do you have a single function in your call stack? Do you also handle/swallow every error in every function or do you propagate those errors? So function q throws errors it was called by function p that throws and by the time you get back to a the list is a lot longer than x, y, z.
Formal verification is a red herring.
I know my software either handles all the errors that it can or stops processing and informs someone. Error types don't remove complexity or make humans better at error handling.
I don't understand the point of exceptions. It's like saying I want a status reporting feature with power and flexibility and meaning, but not that much power and flexibility and meaning.
It seems most people arguing against this clearly didn't read the post or they don't understand the context and are giving their opinions on exceptions in general.
The crux of the post is to leverage the fact that your controller code (in typical modern MVC frameworks) doesn't run in a vacuum, to reduce a lot of boilerplate/repetion around error handling.
For example, in this case: the claim that error handling is “centralized”...how can you be sure? It is generally trivial to locally ignore or capture errors that may not propagate even if they should. Dependencies may not be well-behaved. If you cannot force (or fake) key conditions that “should” cause a component to fail, you don’t really know how that error affects the system or if it can even be seen by the “centralized” handler. One of the issues with exception handling is that programmers may think synthesizing each exception “tests” the condition, when you may not even be able to prove that the real error will lead to the expected exception. You have to be able to simulate or force the bad behaviors themselves, independent from your exception hierarchy.
I’m also skeptical that having local error code “disappear” is a net win. When debugging, you’d probably have to add it all back to know how the heck an error might have originated from that local code. At the very least, local code to reveal errors should never go away.