> Here’s the big lie: in Python, this snippet will almost always raise an Exception. That’s because most users, faced with an input prompt, will press Ctrl+C or enter a non-URL. And even if they enter a URL, it probably has a typo.
[citation needed]
> In English, “exception” means, “something unusual.” But if this code snipet raises ValueError 60 per cent of the time and prints the contents of a web page 10 per cent of the time … well, surely we need a new term?
The purpose of this code snippet is to download a web page and show it to the user. The code considers situations that lead to it not fulfilling its purpose “exceptional”. If someone exits when they see a prompt for a URL, or type gibberish, why did they run a program to download a web page in the first place?
> So you think you know what an “exception” means?
An exception is when code can no longer do any meaningful work.
This could be because of a programming bug, unexpected input, network issue, hardware issue, etc. The cause doesn't really matter. What matters is that there is no way forward for the code.
And this is a decision that is made (or not made) by the programmer. In this author's Python example, he chooses accept the input as correct in the rest of the code. If a valid URL is not provided then the code cannot continue because it is assumed correct. One could easily change this code to have it continue; have it check for a invalid URL and reprint the prompt. However, this would be a different program with different requirements.
FWIW I didn't particularly like the article, but I really liked your comment. I think "an exception is when code can no longer do any meaningful work" is a great definition, and also explains why flow control with exception handling works the way it does.
Obviously there are gray areas re what it is to mean "code can no longer do any meaningful work", which is why there are decisions to be made about when to throw an exception or when to do something else. Usually, though, that "something else" is to return an error sentinel value, most often null or similar (e.g. "undefined"). And then I think the reason it's sometimes a tough call is that since null/undefined essentially means "empty", the APIs that most often return null do so in cases where you're asking for something and it's not there, so returning "empty" feels reasonable.
> I think "an exception is when code can no longer do any meaningful work" is a great definition,
I'm not so sure: that definition glibly turns every error or failure into an exception.
Is "Failure to Open File" really an exceptional condition? This is a common condition that is basically a business rule[1].
Think about the "saving a file" action. There's errors that you expect to happen, and have to provide code to handle (like, permission denied, path exists and is a directory, etc), and exceptional conditions that should never have arisen in a program which no sane person would try to handle (Out of memory, disk write errors, etc).
In both cases, no progress can be made and the callee has to return to the caller. But, in the error cases a well-written program will defend against the errors in a meaningful manner, whereas in the exceptional cases there is literally no point in putting in code to handle those cases.
Just some food for thought - I'm not particularly married to these definitions.
[1] Here are some hypothetical options for failing to open a file:
1. If the file doesn't exist, prompt the user asking if it must be created, and proceed only if "Yes" is received, return to caller if "No" is received.
2. If the file is a directory, display a fatal error message and return to caller.
3. If permission is denied, display an ACL error message and return to caller.
I have a desktop application with a single error handler at its event loop. All that handler does is show the exception message in a messagebox and then the event loop resumes. This application has a "save file" option.
In this application it doesn't matter if the permission is denied, the path exists and is a directory, it was a network share that went away, or a disk write error, or out of memory, or a bug. If I press "Save" and the exception occurs, I can read the message and correct the problem if possible, and then just press Save again. It is incredibly robust and has effectively no error handling.
Of course, that doesn't handle all the cases that you describe. For example, for your case #1 I think that isn't a case that no more meaningful work can be done and isn't an error. Instead, you're just asking a question and then proceeding or not based on the answer.
In .NET, they have 2 sets of parsing functions, "Parse" and "TryParse" for parsing strings into other types. The first throws an exception if it can't parse and the second returns a Boolean indicating success or failure.
Which function of these you use as a programmer is dependent on the context and it indicates your expectation. You might use "Parse" if you reading a well-defined file format where the string will always contain an integer and if it doesn't then something has gone horribly wrong. But you might use "TryParse" on user input where getting a non-integer is expected and you'll have to something about that other than error out.
Was this a workaround for frequent exception handling being too expensive on some platform? I like how Java lets some exception instances be reused without stack frames, so performance issues are less likely to compromise API design.
It, according to Eric Lippert, was at least partially to fix the problem of vexing exceptions[1] that plagued Int32.Parse. Parse was first and it was kind of an unfortunate design decision to make a almost expected result (parsing to fail) throw an exception.
It's worth noting that nowadays Parse is just a wrapper around TryParse that throws (although this is an implementation detail)
I think the causation runs in the opposite direction. These exceptions for these platforms were implemented in an inefficient fashion BECAUSE they thought that exceptions should almost never be thrown.
Semantically I think it's great. .NET has a bunch of these especially in LINQ.
A parsing failure isn't necessary an error. Sometimes it is, sometimes it's not. Even if exceptions weren't expensive, catching exceptions on all parsing (as you would in Java) is just ugly and, I would argue, not semantically correct. In my opinion, one should throw often but catch infrequently. Most of my large applications have a very small number of catches.
It's also perfectly reasonable to move this down a layer and have Parse return an Option type and choose to handle did-not-parse case or don't and let that trigger an exception.
> An exception is when code can no longer do any meaningful work.
That is quite obviously not true, not even on the surface. I have written tons of code where I've caught exceptions and then continued to do meaningful work.
The author's Python example even contradicts your statement. Sure, maybe they should validate the URL first. But even if they did that, the server could be down, the user's internet could be down, or there could be a typo in the URL that makes it unreachable, even if the URL itself is still valid.
And that doesn't have to mean the code can no longer do meaningful work. Maybe the exception handler could consult a local disk cache, and read the contents of the cache and display that instead.
I think exceptions could (and perhaps should) only be used when code can no longer do any meaningful work, but in practice that is very much not how real-world software works.
Ultimately, though, I prefer languages that don't have exceptions. Errors should be passed as return values, preferably using language features that make it impossible to forget to handle them. If they truly get into a state where they can't (or it's unsafe to) do more meaningful work, then they should abort() and the program should be terminated.
The code that catches an exception can continue, but the point was about the code that threw the exception. You throw an exception if you can't continue the work you're currently doing, for whatever reason. Of course, the rest of the program can very well continue: some other part will catch the exception you threw and decide what to do next. Ending the entire process is an extreme case, that should be reserved for only global problems - out of heap space errors, memory corruption, security violations, deadlock detection. Anything else should result in a more localized failure - killing a thread, a task, a request processor etc.
Also, there is little fundamentally different between throwing an exception and returning an error result. Everything I wrote above applies just as much to error results as it does to exceptions. Exceptions are just a language-level mechanism to help with a common error pattern: the code that detects an error condition is typically very far away from the code that can handle that error condition. When an error happens, the vast majority of the time, what happens next is that the error will be cascaded back through the call chain to a handler function of some kind. Along the way, some context will be added to the error, and some resources will be cleaned up. You can do this by hand, as you would in Go or Rust, or you can have the runtime do it for you, like in Java or C#. There are pros and cons to both (implicit vs explicit, how relevant is the context, etc), but fundamentally the program will take the exact same path.
> Also, there is little fundamentally different between throwing an exception and returning an error result.
Thank you. I was about to reply the same. The most notable difference is that from a usage-perspective is that exceptions bubble up automatically, whereas errors must be returned manually.
I usually don't follow the error vs. exception debate because both have their difficulties and both let's you write code that behaves different than you anticipate.
I prefer exceptions, especially how Python handles them (most of them at least). The only thing I really would love to see is something like Rusts Option() type in Python so assigning values to variables inside a try: would not require me to have variables with typ int|None. That's really yuck.
> But even if they did that, the server could be down, the user's internet could be down, or there could be a typo in the URL that makes it unreachable, even if the URL itself is still valid.
That doesn't contradict my statement. In all those cases, the code cannot continue to do anymore meaningful work. Handling the parsing error just handles that one situation.
> I have written tons of code where I've caught exceptions and then continued to do meaningful work.
But the code that threw the exception could not continue to do meaningful work. You can the catch the exception and do something else or maybe try the same operation again. But either way, the original code could not and did not continue. In general, you have to reach higher up in the call stack before you find a spot where you can something different or retry. This is why exceptions are useful.
> That doesn't contradict my statement. In all those cases, the code cannot continue to do meaningful work. Just fixing the parsing error just takes one such condition out. I never claimed otherwise.
If you continued reading to the next paragraph, I presented an option for how the code could continue doing more work.
> But the code that threw the exception could not continue to do meaningful work. You can the catch the exception and do something else or maybe try the same operation again. But either way, the original code could not continue.
But that's not what you said! Or at least what you said was ambiguous. "The code can no longer do any meaningful work." What code? I interpreted that as "all the following code", which feels like an entirely reasonable interpretation to me. I feel like this just proves the author's point -- it's ambiguous!
Ok, so then what you really mean is: "the code that threw the exception can no longer do any meaningful work". Ok, sure, that's true, but I don't think that's a useful definition. I suppose perhaps it's useful if you're explaining it to a non-programmer who is unlikely to need any more information (arguably, you shouldn't bring up exceptions to a non-programmer; just tell them "the code encountered an error and couldn't continue anymore"). But if you're talking to a programmer, or, rather, teaching a new programmer, that definition is entirely insufficient. Because from there, you need to talk about why the way to signal that inability is to use an exception. You need to talk about other ways to signal that, and what the trade offs are. You need to dig deeper. At best your definition is only the very very very beginning of a longer conversation; it doesn't give enough information on its own.
> What code? I interpreted that as "all the following code", which feels like an entirely reasonable interpretation to me.
It is. As long as you define "all following code" as the code that would execute from that point forward. If you define "following" as simply the code in the rest of the file then doesn't really make sense in this context I don't why you would mean that.
Obviously the existence of catch statements allows the program to continue from that point but none of that code is following from where the error was (unless you call it again).
> At best your definition is only the very very very beginning of a longer conversation; it doesn't give enough information on its own.
If you want me to write an essay on this, I have actually already written one. But I still think this definition is a good one. It is simple but that's what makes it useful. The entire subject of error handling cannot be distilled into a single sentence definition and that is not what I intended to do.
>>> When I shifted from Rust to Python, I tried to write matcher-style code. It wasn’t legible. I became irate.
so, in other words... rust developer tried to write rust in python, and it didn't work.
wasn't that the same story for each combination of two languages? I remember people criticize other people for "always writing java in any language they work with"...
> I remember people criticize other people for "always writing java in any language they work with"...
Apples to oranges. When people say “writing Java in any language…” they mean it usually in a derogatory term, as in overly verbose variable names, AbstractSingletonProxyFactoryBean, etc. When people talk about Rust they usually mean functional style, exhaustive checks, safe constructs, etc.
> If you’re using Python, you’re stuck with the endless debates. For instance, you’ll never know why there’s a dict.get() to avoid KeyError but no list.get() to avoid IndexError.
Probably for the same reason that, in Swift, dictionary lookup returns an Optional but Array traps on out-of-bounds: dictionary keys tend to come from outside the dictionary, but array indices tend to be derived from the array bounds. This means a dictionary lookup failure tends to be expected, and an out-of-bounds array index tends to be a logic error. More details here: https://stackoverflow.com/a/75780575/77567
I will paraphrase the Wikipedia definition (itself from Flaviu Cristian and John B. Goodenough):
Each procedure has a precondition, a set of circumstances for which it will terminate "normally". The set of "normal" circumstances is subjective and defined entirely by the programmer. An exception handling mechanism allows the procedure to "raise an exception" if this precondition is violated. The exception handling mechanism then specifies how the exception is handled.
I think this topic has been discussed countless times, and this post fails to add anything worthy to it.
My two cents: use result types, or their analog in your language for expected error conditions, and use exceptions for exceptional situations. Example for the former is parsing, which has an expected failure mode - something couldn’t be parsed as intended. Example for the latter is a network issue during an API call/writing a file/etc. Nonetheless, they are a homologous structure to a Result type in more FP languages, so it’s purely which is a better fit for the given circumstance.
I would also like to defend exceptions, as they are a very good construction with sane and safe defaults. They let people concentrate on the actual business logic, not sprinkling little bit of business and error handling logic all over the place; they do the sane thing by default: auto-unwrap the correct result, auto-bubble up if not handled at a given scope; and often unappreciated: they allow as fine- or course-grained error handling as needed, while it is only possible with some less than readable Monad magic with Result types. And most importantly, they never let an erroneous condition silently go unnoticed, you have to deliberately ignore them for that. With errno checking (c, go), and a bit less so with result types you can fall victim to that.
Edit: Oh, and they add stacktraces! That’s also a very important and useful feature of them, much more so than “grepping for this hopefully unique error code in the codebase to find where it might come from with zero further detail”.
> They let people concentrate on the actual business logic, not sprinkling little bit of business and error handling logic all over the place
This can lead to trouble if the programmer doesn't keep in mind that various (but perhaps not all) operations can throw. In particular, proper behaviour in the exceptional case may require a more specific order of operations than is needed for proper behaviour in the normal case.
Raymond Chen did a great blog post on this in 2005, Cleaner, more elegant, and harder to recognize. [0][1]
The main problem I have with exceptions is that libraries can't be trusted to only throw exceptions in exceptional situations, or even properly document the situations in which they throw exceptions, so my code must be constantly paranoid about every call into library code. Contrast this with languages that provide result types and the certainty of either handling or passing up the error, particularly when the language provides a clean construct like ? in Rust - I just really like the feeling of certainty I get when writing that code over code that can throw exceptions.
I just assume every piece of code can throw any kind of exception. If some code doesn't throw an exception today, it might change tomorrow. It ultimately doesn't matter. There is no need to worry about it.
There are only key points in any application where you can properly handle errors/exceptions and only certain errors/exceptions that can be handled in a specific way. Neither the location of the handler or can be handled has anything to do with what code triggered the error.
Explicitly declaring exceptions/errors completely breaks polymorphism. The library that you call exposes it's internals that way and if those internals change then your code breaks. Either that, or to maintain a consistent API, the library has to lie about the errors it produces by hiding the real error away in a more generic error type.
I disagree with most of your arguments. I think that exceptions are obviously part of the API - they just tend to be a lot more hidden and less thoughtfully designed than the standard interface.
Wrapping or mapping internal exceptions to a API level library-specific type isn't lying, it's just basic data modelling - the same as mapping e.g., a database response to a HTTP response, including database errors to client-appropriate HTTP error codes
And error handling can and should frequently be managed at the call site into a library, even for exceptional cases. Consider transient errors like network failures - it's standard to handle and retry at the call site with a delay rather than immediately propagate to the caller.
All of this is made simpler and more explicit with the use of Result types or similar over Exceptions.
Fundamentally, I think Exceptions don't simplify anything, they just hide complexity - and Result types don't complicate code, they just describe the otherwise hidden complexity.
I disagree because the complexity doesn't exist. If you concentrate only on the places you can handle errors/exceptions and what you can handle that is significantly simpler than fussing over the individual error return types of every single function call everywhere. That's just a make-work project. The big advantage of exceptions is that you can mostly ignore all that noise.
The fact that some function somewhere can trigger a network exception has nothing to do with where I can handle it and do the retry. Even network failures are rarely handled at the specific exact function that calls the operating system to put a byte on the wire. It's probably at least 5 levels up before it even gets to my code.
Exceptions and properly using Result types are effectively equivalent. Add checked exceptions and they are almost exactly equivalent. But that misses forest for the trees.
> fussing over the individual error return types of every single function call everywhere. That's just a make-work project
Yep, it's a lot of work. But if you don't do that work of defining a behavior for every error, you create undefined behavior.
Undefined behavior is acceptable in some places (e.g., desktop apps; small web services); it isn't acceptable in other places (e.g., big web services). At scale, you have to know what happens for all inputs. In my experience, this is easier to do if you figure out the error hierarchy ahead of time.
I am the original article author. I was writing after authoring a new RabbitMQ client. Existing clients were throwing exceptions in the wrong places, leading to website stalls. Stalls happened in every client library I could find. The trick is that the RabbitMQ protocol behaves like an actor system: error messages aren't always tied to requests. It took careful error design to map server messages to exceptions, and that error design had to happen at the very start of the project.
Abstracting a bit from that experience:
* exceptions are "error + goto". The goto saves work 99% of the time; the other 1%, it's awful.
I believe that there are exceptions to every rule :). I'm not going to argue with your experience with RabbitMQ and your solution is probably totally correct. But I think making that a general rule is a mistake. I write all kinds of software from web applications, libraries, desktop applications, and even embedded software and there is no one single right way to do error handling. However, 99% of the time, think exceptions with a very small number of catch blocks is easiest, cleanest, and most robust way to go.
I don't believe exceptions create undefined behavior -- the behavior is very much defined. I know exactly how all my apps will react to exceptions I've seen and not seen. I also know exactly where those exceptions will be handled. I'm also very comfortable knowing that there is no error situation that will be not caught and dealt with in some way. I'd like to know why you think differently?
I think the exceptions are a goto has already been debunked enough. Whether you use exceptions or not, your errors will be handled in the same place in the code. It's just one place you're wasting effort manually propagating that error to that same place and in the other you are not.
The most naive implementation of exceptions would do exactly what the top code does. Check for an error result from the function and return it to it's caller until it finds a catch block.
The advantage of exceptions is that you don't get the bug that exists in functionA where it fails to propagate the error from functionD. It also makes the logic of the code much more readable and writable because you don't need to put an error guard around the call to every single function.
But what is telling is that, in both examples, the error is handled in exactly the same place in the code. How can one reasonably say the second case is like a goto but the first isn't?
I’ve been programming for many years and never thought of or met any issues with exceptions. It’s a useful mechanism to stop execution and send a structured signal up the call stack to where you expect it. If you don’t have issues (e.g. data, syntax, performance related) with the way you use exceptions, it’s safe to ignore all philosophical distresses.
What does it actually mean? When should I use it? Is this correct? Is my usage aligned with the idea behind it? These are all questions of a restless mind. The one that stands in front of a toolbox and cannot choose a type of a hammer for a trivial job where even a rock will do.
> Python, C++, Ruby and Java all use exceptions. To them, “exception” means, “nifty piece of syntax kinda like goto.”
Not Ruby. In idiomatic Ruby^ exceptions are for exceptional cases, not code flow. "nifty piece of syntax kinda like goto." is the much underused but very powerful catch+throw.
Or rather, it's a stack unwinding mechanism: think baseball, the catcher expects a ball, with the ball being a message to be passed. Upon that you could implement a specialized and more costly case of stack unwinding which does what fail/raise+rescue does, but catch+throw is more general than exceptions.
Take Python's StopIteration as an example: there's absolutely no need for this to be a full exception, instead it's like `break`ing a loop but across method calls.
yeah.. StopIteration is not an instance of Exception, is a subclass of it :^)
no, really:
help(StopIteration)
class StopIteration(Exception)
| Signal the end from iterator.__next__().
|
| Method resolution order:
| StopIteration
| Exception
| BaseException
| object
You can have the same philosophical distinction over the term "error". Is it really an error if your computer did not literally catch fire or otherwise took physical damage?
And yet this hasn't stopped even nontechnical people from using the term without any significant communication problems.
I think a more interesting discussion might be what exactly the difference between "errors" and "exceptions" is in computing, though I'm not sure there is one in practice. (I think in theory, "errors" are supposed to be caused by factors external to your program, like hitting memory constraints, while "exceptions" are internal to your program? I'm not sure if this is universally true though or just a java-ism - and even in java it's not consistently followed through everywhere).
So until this is decided, I think the most useful practical definition of exceptions is "it's a special language construct that makes it easier to manage errors".
There was a book “C Interfaces and Implementations” by David R. Hanson and he put it so: there are user errors (input/data errors), program bugs (things we 'assert') and everything else are exceptions. So a non-existent file is a user error, uninitialized memory is a program bug, and an arithmetic overflow is an exception. Not sure if this is useful.
I myself think errors and exceptions are misleading terms. There is an instruction; it does this and that and can produce the following results. We call some of there results errors because usually we imply a goal. But instructions do not really have goals. We search for a key in a dictionary and either find it or not. Either result can be an error or not an error depending on whether we expect the key to be there or want to make sure it does not exist. Yet the searching instruction is same. There is no reason to prefer one result or another. It is more important to make all the necessary distinctions.
> There was a book “C Interfaces and Implementations” by David R. Hanson and he put it so: there are user errors (input/data errors), program bugs (things we 'assert') and everything else are exceptions. So a non-existent file is a user error, uninitialized memory is a program bug, and an arithmetic overflow is an exception.
I don't agree with this definition in the same way I don't agree with many others not found in books.
These definitions always reflect the personal view of their authors, but are fundamentally just an interpretation of their own thoughts. There is nothing objective about it. It's just a philosophical or semantic debate.
For example, why is uninitialized memory a bug, but an arithmetic overflow is not? In both cases it would have been the developers responsibility to initialize the memory and prevent an arithmetic operation that overflows.
In his interpretation exceptions were just a little short of being fatal errors. They were almost fatal errors but with an optional escape hatch. But I agree all these distinctions are vague and do not seem to be fundamental differences.
Exceptions are a side effect of nice syntax. When we write
a = b + c
in a modern language quite a lot can happen behind the scenes. Yet the form implies 'a' will always be a sum of 'b' and 'c' whatever this means. There is no notion of 'a' not being a sum. So it seems there are two ways: either we provide a separate error handling mechanism (exceptions) that stops the execution and leaves 'a' undefined or we somehow turn 'a' into either a sum of 'b' and 'c' or an error (sum types).
Errors have the connotation that something has gone wrong, exceptions do not. Exceptions as used in CS also do not have the connotation that they are unusual, as pointed out by the article. But generally, every error can be made into an exception.
As far as deciding on what they mean, I have brought up ChatGPT's definitions in arguments and been laughed at. People seem more interested in arguing than in making their vocabulary precise.
[citation needed]
> In English, “exception” means, “something unusual.” But if this code snipet raises ValueError 60 per cent of the time and prints the contents of a web page 10 per cent of the time … well, surely we need a new term?
The purpose of this code snippet is to download a web page and show it to the user. The code considers situations that lead to it not fulfilling its purpose “exceptional”. If someone exits when they see a prompt for a URL, or type gibberish, why did they run a program to download a web page in the first place?