Exceptions should be Exceptional (2016)
mattwarren.org
mattwarren.org
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.
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.
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.
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.
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.
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.
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.
And so, the debate continues.
https://docs.python.org/3/library/functions.html#open
"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.
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".
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.
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.
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.
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 personally like Go's approach, but that's not for everybody (I'm also quite fond of the monadic approach though).
Fact 2: When there is an unforeseen event, the developer did not consider what to do.
Fact 3: Developers often deal with other people's buggy code.
It's clear that exceptions aren't the perfect solution to anything, but given these 3 facts, exceptions are around to stay.
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.
Oh well, they can't all be winners.
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.
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.
An extension like
> dict<k,v>.GetOrDefault(k key,v valueIfNotPresent)
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.
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");That's kind of the problem though. Exceptions are flow control.
...for exceptional events.
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.
However, Maybe can be implemented without tagging, see[0] for example.
I'm pretty sure the cost of checking this in a static language where the tag enums range is known at compile-time is:
- 100x cheaper than cache-miss on a memory access
- and 10000x cheaper than IO which is often where those errors/results/maybe/exceptions issues arise.
I.e. irrelevant in most cases.
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...)
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.
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.
Result<T, ErrorT> is used in place of exceptions, much nicer IMHO.
If it's the only type of error your method returns you don't even need to, just define your return type as your custom error type.
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!
Can you explain what you mean by this? I certainly use them in a manner I would consider polymorphic.
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.
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 :-/
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.
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.My gripe with it is (only for php), it's not visible from the calling side that we are passing a pointer.