When your main argument for not using exceptions for every kind of error handling is performance instead of architecture, then maybe it is a sign that exceptions should simply be constructed to be more performant instead of giving people advice that isn't and can't ever be consistent.
As the writer of a library (or just other utility functions), you simply cannot know whether something is an exceptional situation or not, because that is a property of the program and the environment, not the function. Being unable to parse a string to a number, or to create a socket, or to even allocate memory can all be exceptional or not exceptional depending on the context, and often you simply do not know the context this code will run in, or the code will actually run in multiple directly contradictory contexts.
As a library writer you can let the caller the responsibility to determine if something is worth throwing an exception for or not.
1. return an error code
2. if the caller consider the issue to require an exception, they will do it themselves
You can also have entrypoints to the library that will throw exceptions, and other versions that won't, and let the caller decide what they want to use.
Though of course that's more work.
Regarding the point of performances, that makes me think of https://news.ycombinator.com/item?id=22483028, discussing "Low-cost Deterministic C++ Exceptions for Embedded Systems". It would definitely be very interesting to see what is now possible if exceptions are cheaper.
The problem with doing that as a library writer is that exceptions, used well, generally change the entire interface and to some extent programming model. It starts with simple things like returning values instead of an error code (and setting value by reference) and goes through having to make "factory" functions for classes (if you want to use error codes) and ends with all sorts of considerations like copy constructors in C++.
So in many cases, if you don't want to compromise on design and architecture, you essentially need to write the entire library twice, or at least the entire interface. Not really a great state.
>discussing "Low-cost Deterministic C++ Exceptions for Embedded Systems"
This exact reason is why I think that paper is great and also how exceptions should have been implemented in C++ in the first place.
I totally agree for things like streams or IO like file handling or authentication. It may be fatal at the IO layer but in a GUI you want a nice error displayed to the user and not a crash.
However in some domains you don't want to pollute your return value with a Result or Error enum, for example a BigInt library addition or a Matrix addition would be much more convenient for scientists to use if it returned a BigInt or Matrix respectively.
> When your main argument for not using exceptions for every kind of error handling is performance instead of architecture
Personally I find that using exceptions quite broadly makes it easier to build fault tolerant software. And to me this makes the performance issue rather moot. Some classes, such as, financial OLTP systems, should much rather be correct and resilient than performant.
I could presumably write the same exact logic in asynchronous C, but most likely it will have serious stability issues. And what is more, every person I have come across in my life that insisted on writing asynchronous code for financial OLTP systems ended up writing software which would never have the requisite stability and had to either be abandoned or rewritten.
To me, avoiding exceptions absolutely seems like trying to fix a problem that is not related to how code is written by changing how code is written - instead of changing how the code is executed, which is where the actual problem comes in.
Another argument I hear all too frequently though is that people can just ignore exceptions ... and yes - they can - and in almost all cases (unless someone did not just ignore exceptions) this will cause program or thread termination.
In C you can just ignore error return values also - but what is worse - you can forget to check them - and then execution continues as if everything is fine. With exceptions you guarantee that someone has to go out of their way to continue execution as if everything is fine if something went wrong.
This is why exceptions are optimal for happy path programming.
An exceptional condition is arguably anything that means some call in your program can't do its job.
Passing error conditions up the call stack to the control layer that displays and/or logs a relevant message is a pain.
Exceptions make it less painful.
Catch blocks should be used sparingly, sure. If you're using them close to where the error is thrown then maybe you should consider an error code-oriented approach. But catch blocks in controller code (or better yet, meta-controller code)? Sure.
The problem is that one person might think an exception is acceptable for OpenFile, whereas another thinks it's not. It's this ambiguity that makes for all sorts of conflicting ideas and recommendations.
Fun Fact: the Windows API uses HRESULT to return success/failure of a particular call. The COM API uses the same. VB6 was the first platform by Microsoft to implement exceptions throughout the code (that I'm aware of)... and beneath it, it used COM. So that means that VB6 generated an exception whenever a COM status code returned E_FAIL or similar, and didn't if it was classed as SUCCESS (eg S_OK, S_FALSE, etc).
The original .NET implementors clearly used the VB6 model when developing the codebase (it was nearly a one-to-one match from VB6 to VB.NET) and very easy to port over.
That said, when I read the article, "Exceptions are the primary means of reporting errors in Frameworks" vs "Do not use exceptions for the normal flow of control, if possible" I see this never ending problem appear. Most things we do in our programs use Frameworks, which throw exceptions, which may or may not actually be exceptional for the case of the calling function. Nevertheless, it must be handled.
However, back in the C++ Win API and COM world, you could easily use: `HRESULT hr= SomeFunc(); if (FAILED(hr)) return hr;` to bubble up a failure code, or if you handled it in other ways, you can safely ignore the return code and continue on. In the .NET world exceptions must always be handled otherwise they ripple all the way up and end up as a crashed application. Of course, some think this is a good design principle to have it so: Others are not so convinced.
"Open file and return a corresponding file object. If the file cannot be opened, an OSError is raised."
in the sense that in a program that is not only a few lines, I would prefer such kinds of library functions not to produce exceptions, thank you very much, so I had to wrap all such. But it is not optimal not being able to directly use language primitives as much as possible.
On another side, writing really short programs gets to be faster: if I write only a few lines, having an "implicit" error handling shortens the code.
And that's what I think is the main reason for many ills and misunderstanding in many popular languages: the practices that look good for small examples (or in the books) aren't the same as those which work well for large projects. Python's not detecting the typos in the variable names in compile time is also a typical bias towards the short programs.
For large projects detecting as much at compile time and having only very exceptional exceptions and unsurprising control flow (visible reading ifs, and with a guarantee that there can't be "landings from anywhere" like in exceptions) is of huge advantage.
Python is far more promiscuous with Exceptions than languages like C#; they're not just used for "truly exceptional" conditions (see things like StopIteration or GeneratorExit).
Different languages have different attitudes to this and that's reflected in their APIs. This will naturally affect your choice of language when designing a system, depending on your tolerance for failures, your demands for robustness, or your definition of "exceptional".
Exceptions when opening a file has been chosen because they have the nice propery of encouraging a race condition free EAFP rather than a hard to get right LBYL.
It also fits nicely into the context manager pattern.
Besides, remember exceptions are cheap in python. So cheap that for loops are basically syntaxic sugar for a try/catch.
EAFP - Easier to ask for forgiveness than permission
LBYL - Look before you leap
They are opposite strategies, Python's preferred strategy is to do the first, just try the operation and throw an exception if it doesn't work. The second is to check a pre-condition before attempting an operation.
I've always known the second as exhibiting TOCTOU, otherwise known as a time-of-check vs time-of-use race condition.
But that is a false claim. It's not about being EAFP or LBYL: if the open API returns a handle to file or null, the open operation can still be atomic and therefore race-free.
Which is exactly how it's in all the programing languages and operating system calls which don't use exceptions for that.
It's just the question if one has to write try-catch for something as common as file not existing.
LBYL can easily be enforced by Maybe types, leaving exceptions for really exceptional things (which a nonexisting file definitely isn't). So we see how not having a real type system hurts and deforms a language in ways unrelated to types.
"Exceptional" has no reasonable objective definition, especially not at the library level. When you try to open a file by name and it doesn't exist, is that exceptional? It depends on context. Whether something is "predictable and intentional" can even change with product evolution. It's a recipe for bikeshedding. This is a crazy criterion to try to make coding decisions by.
Different applications need different trade-offs, and not every API will be suitable for all users. Standard libraries will make decisions according to what kind of applications the language targets. Libraries making other trade-offs can be written to cater to people with other needs.
I get that some people are repeatedly encountering situations where they feel like exceptions are used in ways that reduce readability and performance, and they feel like this is a rule that could improve the situation by reducing the use of exceptions across the board, but please, apply your usual criteria of clarity, readability, and performance! If you believe that exceptions are the clearest way of expressing a certain bit of logic in your language and codebase, and that they are acceptably performant, don't get distracted from that by bottomless, ultimately insubstantial arguments about what it means to be "exceptional."
Edit: Just wanted to add that I really enjoy the more expressive type systems that make it ergonomic to use return values in a lot of situations where exceptions are otherwise the most readable choice.
Maybe the issue is that "exception" is a really confusing name. Go has the keyword "panic" for errors that stop the current flow and unfold the stack, that makes it clear that you shouldn't use them for anything else than exceptional situations, and they can be "recovered" if they have to be handled explicitely.
From my (admittedly limited) experience, Go has the worst story for error handling, where functions end up repeatedly checking for error return conditions from called functions.
It feels trapped between "We don't want exceptions" without going full "Result<T> or Maybe<T>" functional style that might be more typical of something like elixir.
But I didn't try Go for long and that itself was years ago, so perhaps modern Go has better paradigms.
I'm just mentioning the naming, not the general approach to error handling. Panic are roughly similar to what an "exceptional exception" is (stop control flow, unfold stack until being handled), but the naming is slightly better IMHO.
I personally like Go's approach, but that's not for everybody (I'm also quite fond of the monadic approach though).
Relatedly, I'm a strong proponent of debugging with "break on all exceptions" enabled. It's very easy to miss bugs if you're not aware of exceptions being thrown but for this to be practical the code must only throw exceptions exceptionally or it's a nightmare to debug.
I wonder why callbacks aren't a more popular way for languages to handle exceptions. What if each type of exception registered a weak link to a function with a name like `IOExceptionHandler` which prints a noisy error if it isn't overridden with other behavior?
I guess it would be harder to use a single exception callback if you had a lot of similar "try/catch" blocks in different areas of your code, and you'd still probably need to collect some sort of stack trace, but it might work out alright if you go with this article's advice and recommend that people keep exceptions out of their program's normal control flow.
I dunno, maybe it's a dumb idea and I've just been spending too much time with microcontrollers lately. They use interrupt callbacks for all kinds of hardware triggers and software events, and I kind of like how you can compartmentalize error handling into a few predictable event handlers.
That Java stack trace image at the end of the article also brings back some memories...but I'm feeling much better these days.
That is sounds more appropriate for either terminating the program or resumable exceptions like division by zero. In languages with a try-catch construct (specially C++), the complexity of implementing exceptions has to do with stack unwinding. This is where you don't want to restart in an outer context, but want to guarantee cleanup for the call frames you are abandoning.
> So exceptions should be exceptional, unusual or rare, much like a asteroid strike!!
That's a super huge misread of the text recommendation.
I watched a live presentation directly from Jeffrey Richter at the bay area .net user group, oh god about 15 years ago when he described how he came up with this text. In fact it was a call to use exceptions a lot more than we used to, and to avoid error codes. So no, exceptions are not supposed to be rare like that.
His main point was to throw if the method name was not achievable, except in specific situations where that breaks user expectations too much, like getting null from missing map entries.
Anecdotally one of my stumbling blocks with clojure was how to deal with errors. The language style heavily pushes to just coding the happy path and using nil punning, instead of exceptions, but sometimes you need to signal errors, so some libraries use keywords for error codes - but now that we have spec the community seems to be shifting opinion to “exceptions are ok-ish if a result of not conforming”. Both error codes and nils make spec definitions ugly, thus all the “maybe not” opinion by Rick... it seems someehat overwhelming, to be honest.
Yes, for the indexer scenario dict[key] that is an exceptional failure (equivalent to indexing list[index] wher index >= length). The method to get the value of a key only if the key is in the dictionary is TryGet...
I think this design is the best one. It lets you have the program blow up quickly in all cases where the presence of the key is a requirement for the program to be in a valid state (which is common). Otherwise you'd be doing
x = dict[key]
if (x == null)
throw("Unexpected this is. And unfortunate");
Yes and probably make the most sense as .Net has no "undefined" value. If the dictionary has a the key with a null value it returns null, but no key throws.
With generic dictionaries (which everyone uses now days) you have TryGetValue which is a little weird since it uses a out parameter but pretty natural after c# 7 inline out params.
Would be really helpful. It's easy enough to write your own extension to do this, but something built in would be the kind of nice to have method that has been creeping into .net.
No. In the same way that my directions to the mall don't include "maybe take this turn" at all turns.
That is, in many (all?) imperative lists of steps for how to accomplish something, I often do not want to acknowledge that every step could fail. Rather, I would like for a failure to be something I can touch and reason with. Possibly restart from.
Why did I not take that turn? Because I need gas. Or the kids need to use the bathroom. Doesn't matter, too much. I'm the user. Give me my options at this point. (And as a real user of maps, also give me the option of "piss off for 10 minutes.")
(Yes, I like the condition/restart system of common lisp...)
> No. In the same way that my directions to the mall don't include "maybe take this turn" at all turns.
I think that's a really poor and simplistic comparison. You're content not considering every possible outcome of trying to go shopping because in the face of changes to your circumstances, you can easily come up with a new strategy and adjust your goals on the fly.
> I often do not want* to acknowledge that every step could fail.*
That's what you want, but I have often found that the demands of quality software are different. I've seen too much software end up in strange and unconsidered failure modes because it threw up at the wrong time.
I should say that my want there is local to the directions. For the trip, I definitely want them handled.
So, for the function, I want to be able to say the happy path. I would also like to be able to say what could go wrong, with advice to the user on how to proceed.
I don't think options really serve the same purpose as exceptions. If you have are just passing up None or whatever up the call stack over and over again, you probably should just be using an exception. If not, then options are fine. I use options for normal behavior (a missing file, for example) that perhaps can be corrected or handled in a different way. I throw an exception if I want to just crash the program. I rarely ever try catching exceptions, except in maybe some top level driver function. Oh and to all the programmers out there who wrote the code I'm currently dealing with: please don't use exceptions for flow control, it's a terrible terrible idea.
That means those events that are very far from the happy path and need to be recovered by unwinding down multiple function calls until a safe point to recover gracefully is reached. These are events such as failing to allocate memory.
I think those are awful to deal with errors. You have one part of the code that handles one concept and could error something. However the result of that code is then handled to some other part of the code which might deal with a local error but now you really don’t know if the error was local or occurred in the other part of the code. The second part of the code is now dealing with an error situation which it shouldn’t and you’d need to write weird code to stop it from doing that. Also you introduce behavior caused by two different context but merged due to the language “features” which, I think is much worse, because it’s not obvious that it just happened. Using exceptions otoh fixes this and parts of the code can still be completely oblivious that errors can occour.
Which is why Result and Error types exist. With those you get both the context of the error as well as making it clear that the code you are entering needs error handling to be safe. Much less of an issue than calling code that unexpectedly raises exceptions.
And they do have their uses but they are not good enough by their own. Having both exceptions, result types and maybes is a lot better to have and to use appropriately than only have one or the other.
I heard related issues called boolean blindness. You drop every bit of useful error information and leave only result/no result or true/false. Want to load a file and it fails? Access rights? Parsing Error? File doesn't exist? User interrupt? Can all be summed up as an empty Optional, completely useless for any kind of automated or manual recovery or even to decide if recovery is necessary (user interrupt).
and in those langs its trivial and very common to make even more elaborate return/result types containing a very precise representation of all possible result categories.
I’ve taken Go’s approach to error handling in new typescript projects. I want to force my team into thinking about IO errors. I want failures to be part of the domain since stack traces only kinda work in Node. We will only throw exceptions if someone didn’t program for bulls properly. Everything else is a Failure instance passed up the call chain. Annoying, but our testing and code-thoughtfulness have gone up.
Go handles it shitty, like the past five decades of computer science and PL design never happened. For imperative languages, exceptions are perfectly fine as a way to encode failure as part of the domain of operations. If you're in functional fantasy land, the correct approach is to use a Maybe monad or, better yet, an Either monad. But Go supports neither of these and that's one of many reasons why it sucks for modern development. In TypeScript you can do better and, for the sake of others who have to maintain your code, you should do better.
> Go handles it shitty, like the past five decades of computer science and PL design never happened.
This has generally been my complaint with Go. Go is... fine. It offers a lot of features that seem revolutionary if you've never seen them before, but mostly feel like someone took the best parts of every language, and then implemented the least interesting aspects of those features.
- Interfaces are like typeclasses, but without actually offering any sort of higher-kinded polymorphism.
- Goroutines are kind of like Erlang processes, but without any notion of supervision and with shared mutable state.
- The error handling feels like it's trying to emulate an Either monad, but in a way that doesn't actually allow for the compositional properties that make an Either monad useful.
I know that Go's goal was never to innovate on language design, but I feel like it ignored a lot of low-hanging fruit in modern language design.
Obviously you can do worse than coding in Go, but for everything Go is good at, there's likely an even better tool for the job. I'm not convinced that there's any domain where Go is the best choice available, but it's definitely average at most jobs, and maybe that's where it finds its niche.
Edited: I had missed the word "higher-kinded" in front of polymorphism. Thanks donatj!
You're completely right; I accidentally dropped the word "higher-kinded" in front of polymorphism.
Go's interfaces provide structural typing, which offers polymorphism over values. Typeclasses in comparison, offer polymorphism over types themselves. The distinction is subtle, but it drastically limits the sorts of abstractions that you can write with Go. In Go's defence, this is a limitation that even functional languages like F# and Elm face too. This sort of polymorphism is what makes ideas like monads and functors so powerful in a language like Haskell, and what causes those same ideas to lose some of their lustre in a less expressive language.
Unfortunately it's really difficult to find good articles on higher-kinded types, since anything written about them already assumes a ton of knowledge about the subject. This StackOverflow thread has some good insight though: https://stackoverflow.com/questions/21170493/when-are-higher...
It's entirely likely that in the real world these sorts of limitations don't actually matter, but my general frustration with Go stems from the fact that it's so close to being so much better than it is, and it saddens me that we as an industry chose to be so excited by something that's a tiny step forward, when there exist so many other options that are entire leaps and bounds forward.
Yeah, HKT is a godsend. It surprises me that Scala, one of the few languages that actually supports it, doesn't really use monads and all that good stuff in the base language. Like the fact that Scala uses duck-typing for the 'for' construct (it just looks for map, flatMap, filter) has always seemed odd.
The biggest mistake I think go made was not to include codes or domains. The error you get back is... a string. So you have to write tons of if statements just to return a bunch of errors you can’t handle.
Right but then you need to use errors.as to match against it, rather than being able to do pattern matching. And there is no inheritance, no ability to generalize errors into a group.
My experience with Go have been very positive. Once you get over the initial irritation of the error handling, I think it works perfectly fine. I have never been so productive as I can be when programming in Go. I’m sure there are technically better alternatives, but it has a very good balance between features, speed, standard library and multi platform support.
At least for the backend server tasks I use it for.
My biggest complaint has more to do with the new module system and tooling. The (not so) new Go Language Server is still very buggy and almost unusable in my opinion.
Using a Maybe type sounds a bit better, but not really that different than how Go does it in practice. The key decision is whether to throw errors or return them. Go's approach and the Maybe type are both variations on returning them.
The problem with returning them is it's tedious and you end up with error checking boilerplate everywhere.
The problem with throwing them is they're easy to ignore, and also you never know whether or not a function is capable of throwing an error, or what types of errors it can throw.
In conclusion, error handling sucks no matter how you do it :-/
TypeScript is equipped for having and handling option (maybe) types (`T | null`) or result (either) types (`T | Error`), why not handle errors in a proper way ? A way the compiler ensures you handle all cases in every cases ?
If you do it the go way, I assume every fallible functions returns something like `[T, Error]` ? With no guarantees the error is actually checked.
I used to be in exception camp, but I have lately turned to the anti-exception camp. Exceptions have a nasty way of polluting your architecture, they live orthogonal to the rest of the system but still part of the API and have a tendency to mess with every part in every layer & module.
With exceptions is it easy to end up in a situation where your model code changes UI behavior. Let say your model throws ItemNotFoundExcpetion, are you going to catch that in your global handler & return a 404? The proper way is probably to catch it in your controller & rethrow a ResourceNotFoundException to the UI. Thus you need to do that for every exception, catch & rethrow.
And not only you need to translate exceptions between layers & you probably want to translate exceptions between modules. The number of exception classes in your project starts to explode. You have to create hierarchies and groups of exceptions. And many of them are just copies of others.
In a PHP project I experimented with a similar approach as Go, unfortunately PHP does not a have good tuple type support. You can return an array from a function, however the items will be untyped so you have to rely on phpdoc.
What I did instead was to use an out variable for the error.
function parseInt(string $value, ?string &$error): ?int;
$int = parseInt('10', $error);
if ($int === null) {
echo $error;
}
Worked pretty okay. Still undecided if this is a good approach.
As the writer of a library (or just other utility functions), you simply cannot know whether something is an exceptional situation or not, because that is a property of the program and the environment, not the function. Being unable to parse a string to a number, or to create a socket, or to even allocate memory can all be exceptional or not exceptional depending on the context, and often you simply do not know the context this code will run in, or the code will actually run in multiple directly contradictory contexts.