Exceptions are clearly superior to error codes in most cases. Easier to maintain, easier to handle, and you can attach much more contextual information to them.
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).
> Exceptions are clearly superior to error codes in most cases.
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.
The fact they are typically referred to as exceptions has nothing to do with the fact that they are exceptional or not. Exceptions, in an abstract sense, are a language feature that allows exiting from functions via an alternative mechanism to traditional 'return' statements.
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.
> The fact they are typically referred to as exceptions has nothing to do with the fact that they are exceptional or not.
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."
Say you have library that takes a filename and returns the contents after some kind of decoding. If the file is inaccessible (doesn't exist, in use, user doesn't have permission), you don't want the library to take any real action on that. It shouldn't display a message, attempt to correct the filename or retry the same file. It should just let the caller know that an error occurred, and ideally what that error was.
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.)
> Now how is that any different from raising an exception?
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.
> 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.
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.
You might only be able to handle restartable network exceptions. In my case, that might be a single base type. So I put a try block in where I can handle that exception (where my network code starts) and I'm done.
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.
Actually, for the most part I don't like implicit. But I also like encapsulation and polymorphism and you can't have those things if you are exposing implementation details all over the place.
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.
If you want strongly typed errors then you could use a language with checked exceptions (Java). What you are describing is not a complaint about exceptions, it's a complaint about weakly typed errors. You can have the same problem even if you don't use exceptions -- e.g. what happens if one of your inner-inner-inner functions returns null when the docs say it shouldn't? You will just end up with if-null soup instead of try-catch soup if the language doesn't support exceptions.
> what happens if one of your inner-inner-inner functions returns null when the docs say it shouldn't?
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.
I actually think checked exceptions make handling error propagation much easier than Option types. In a language with checked exceptions, if you want to propagate the error, all you have to do is declare that intent. With Option types, there is a need for boilerplate unwrapping code in almost every place where you want to use the value.
> With Option types, there is a need for boilerplate unwrapping code in almost every place where you want to use the value.
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).
> Disagree, in my favorite language you just add "?" to the possibly-erroring function et voilá, error propagated
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.
> 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.
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?
> 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.
So if you have a potential call chain of dozens of methods, your error return includes every single possibility? That's pretty doubtful. What happens is the same thing that happens with checked exceptions; the errors are massaged or placed untyped in properties and while you're handling all the errors you're doing it in a sloppier way.
> So if you have a potential call chain of dozens of methods, your error return includes every single possibility?
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).
Java has checked exceptions (which honestly is more like error-return) and the innerException property is used extensively to hold the "real" exception and, of course, it's type is just "Exception".
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.
> Java has checked exceptions (which honestly is more like error-return) and the innerException property is used extensively to hold the "real" exception and, of course, it's type is just "Exception".
Yeah, well, Java's semantics and programer laziness make a mess. News at 11.
> I'd argue it makes polymorphism completely impossible.
It's like it's 1992 all over again. Every change requires changing every caller everywhere. Yes, it's an improvement now that the typechecker will tell us all the places we need to change but it still doesn't sound enjoyable or useful.
Non-trivial changes to functions typically change what errors they will trigger. Thus changing what errors their callers will trigger. And so on.
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.
If you had an external compiled library the consumed MyError or called caller, you just broke it, becuase you changed the type.
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.
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:
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?
> 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?
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.
Honestly, that second example is more complex. Now you're had to manually map one error onto another in order to propagate it. How big does MyError get when you have OtherError, AnotherError, FailureNum5, ChokedOnBanana. You're just doing busy work creating types and mapping them together. And this is just a simple toy example! You're quick to hand-wave that wasted effort.
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.
C#/C++ do exceptions fine. However, it's more useful to point out the language that does exceptions all wrong: Java. I feel like the current anti-exception sentiment is mostly due to programmers first or major experience with exceptions is checked exceptions in Java.
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.
> 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.
I disagree with this, on a very strong and deep sense!
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.
So division by zero should be handled the same way as network timeout? Because that math code is gonna be verbose.
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.
There are some functions for which we know things will break eventually, e.g. a db call. We want to catch those situations, no pun intended. Our options are: return codes, or exceptions.
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...
The alternative is the Result or Maybe objects with pattern matching from functional languages. It seems similar to return codes with if/then, but the strong typing and the match forcing you to deal with all possible branches (or pass it along) makes it less spaghetti looking.
Meanwhile in the land of PHP I'm convinced that forcing paranoid error levels and converting all errors to exceptions is the only safe way to develop. Too many times in PHP the return type of a function is not what was expected from documentation and I'm not about to do type checking in addition to value checking every function result. Ridiculous. So type-aware comparisons and errors to exceptions it is. Whatever code called a file that managed to hit a bad return type can now continue living and logging instead of `PHP Fatal error`
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.
> forcing paranoid error levels and converting all errors to exceptions is the only safe way to develop
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.
> I disagree with this, on a very strong and deep sense.
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.
> You can't possibly know instantly all the exceptions to handle for a function (given it calls other functions w/ exceptions),
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.
I can make the same argument for exceptions - You're not documenting correctly. You also have the choice to ignore and fail with exceptions.
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.
> 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?
> 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.
Usually documentation tends to have this, or you can look at the function and immediately know what it will return, whereas with exceptions any passthrough errors are transparent.
Exceptions are clearly inferior to an alternative strategy of handling errors as values rather than something exceptional to catch. Exceptions to me started off as a humble concept (if something truly exceptional happens, do x y or z, but in 99% of cases you will probably just want to abort or crash)
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!
Don't confuse the name "exceptions" with the English definition. They are just a mechanism for returning values, usually signifying errors (but not always: see Python StopIteration), without using a standard 'return'.
So no, nobody is considering a network failure an exceptional case.
> 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!
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.
> while ignoring the ridiculous notion that you can know every possible error that every function can trigger now or in the future.
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.
You know every single error every framework function can call? Every single error in your entire code base? What about tomorrow when Bob changes some code?
> 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.
> 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!
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.
Conditions are clearly superior to exceptions in most cases.
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.
Hmm, I haven't given it that much thought yet. Maybe I should start writing integration guides for the major frameworks. You are more than welcome to open a GitHub issue about this, and I'll get to it.
The blog post is proposing exceptions in the context of an MVC style app where the code that throws the exceptions is part of a request pipeline that also has a global exception handler. This handler would be capable of logging and translating the typed exception to some acceptable output to the consumer.
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.
One subtlety is that you had better watch the bad client request rate during deploy. I’ve seen SLOs completely blown and swept under the rug (accidentally) because infra bugs caused “user” errors.
I rarely observe developers investing in forcing the error conditions that they’re supposedly handling. Without that mechanism, your error “handling” might be code that looks good and does nothing (or worse, the wrong thing).
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.
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).