You’re better off using Exceptions
eiriktsarpalis.wordpress.com
eiriktsarpalis.wordpress.com
The way I see it, exceptions should only be used for things that are irreconcilable, which most of the time is interpreter errors(e.g. undefined is not a function). In other words, I don't think it's that common that custom exceptions are needed outside of assertions to prevent the developer from doing something stupid. If you aren't using types, raising an exception can be helpful to provide feedback and useful information when data isn't formatted correctly.
Exceptions suck when they're used for problems that aren't actual problems. For example, if a query is made to an API for a record and that record isn't found, the SDK wrapping that API would raise a "Record not found" exception. What the hell? A record not being found is a normal thing! I've seen this kind of thing all over the place, and I recently had to write some application code to work around one of these useless exceptions. They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself.
It's a fine approach for prototyping but we should be highly suspicious of it for code people rely upon to work. As complexity approaches infinity, the odds of the system being "happy path"-aligned approach zero.
Network-relevant programming (such as web UIs), in particular, really highlights this fact (since any given network request is always allowed to fail, and "user closes their laptop or mobile device, moves somewhere else, and resumes work on an entirely different physical network" has gone from a rare occurrence to an extremely common one in the past two decades).
For recoverable errors, you catch.
For unrecoverable ones, you don't. The exception throwing mechanism unwinds the entire stack and crashes the app with a helpful stack trace.
This is why API generally have checks that allow developers to avoid that failure if the failure is expected (eg Record.exists). What you are suggesting is simply a reverse order (check after vs check before). I think check before leads to a much cleaner API than one that dumps a null or failure code instead.
I don't consider looking for an invalid key exceptional, basically, so I wish the syntaxes were reversed, so I could use the more succinct form for (my) more common case.
The idiomatic way here being that the zero value should be a sane default for that value. You are then free to create your own types ontop of that, but the defaults are often in the ballpark of where you want to be unless you have specific needs in your context.
This is not always the case. In fact, for query APIs I do not expect it at all. Sometimes I'm doing something similar to a UPSERT operation with more nuanced behavior, then returning no existing entry on a preceding SELECT query could be the 99% use case.
> This is not always the case. In fact, for query APIs I do not expect it at all.
And, in your case, "if you expected the record to be found" doesn't apply.
If I'm iterating over a list of names/id provided by the APi, and then calling the API for more information each one, then I would expect the record to be found... because the API just told me it exists. In that case, it not being found is an exceptional condition.
Then again, maybe there are cars built that way. Oy vey.
I would never expect that a call to `Record.find(...)` would always find records, and that the failure to find a record means that something is broken. The absence of data is usually all you need to handle control flow, and treating absence as a failure is presumptuous of API engineers. Having to account for a specific exception just adds more lines of code that could be easily avoided.
A better use of an exception would be for a system failure like this: "Connection Failed"
IMHO, that's being a bit too ideological. It's better to think about it in terms of how you'd want to respond to the condition, and pick your tool appropriately.
For instance, if I'm asking for a record by ID, then a record not found exception is a good fit. I was probably going to do something with the record, but now I can't, and the exception idiom tool the condition to be dealt with where it happened even if I forget.
If I'm asking for a set of records matching a criteria, and nothing matches, then a record not found exception is a poor fit. Code that can deal with a populated list usually can deal with an empty list just fine, and using an exception just adds friction.
If I'm asking for a set of records matching a criteria, and the database is is down, then an empty list is a poor fit. I have information about the connection error I need to communicate, so it's better to use an exception.
This isn't something you can make universal statements about, because it depends on context.
If the program knows it just put or saw the record there, is it strange to assume it's still there? If the program completely controls the database and something else in it implies the record is there, is it still strange to assume?
Sometimes broken assumptions can only signify that something has gone very wrong, and it's not worth handling them at a fine-grained level. That leads to the "gotta catch em all" antipattern the article mentions.
This just isn't true. It depends on what the record is, and what state you're in (aka: CONTEXT).
incredibly clear example: 1. User logs in 2. User record no longer exists 3. Exception
That's not a recoverable situation, and the client should see that as an unexpected error.
> This just isn't true. It depends on what the record is, and what state you're in (aka: CONTEXT).
These two things mean the same thing. It's in the context - > it is not an inherent property.
It's not an inherent property - > it's not always present.
Some conclude therefore it shouldn't be an exception by default.
>Anyone who expects a database to always have records and that the absence of a record is an "exception" has a very strange way of thinking.
But pedantics aside, my point stands. There are absolutely contexts and code paths where a missing record is basically unrecoverable. Why bother returning an error to the local calling context when you know that code doesn't make sense anymore and can't recover? A jump to a new code path is the right call, and exceptions do that (with a lot of nice trace information about why we ended up on that new code path to boot).
Doesn't mean a missing record should always result in an exception, but it's certainly not a "very strange way of thinking".
The world is full of bugs of the form "does this file exist? no? OK, open it for writing", which is exploitable by dropping a symlink in there between the check and the open.
The classic primitive CMPXCHG works like this too; you use it as you would a "store", but one that might fail if another thread has changed a value in the meantime.
It’s good practice to design APIs to make race conditions less likely, by explicitly not splitting operations across multiple calls.
Separating “exists” and “get” into separate calls is a disaster.
I guess databases and file systems are different because concurrent access is the norm, not the exception, and APIs should be designed accordingly. But for most of your in-memory structures, you probably don't want them dealing with concurrency because they aren't going to be able do that very well anyways (instead, deal with concurrency outside of the data structure and have them throw when inconsistencies from bad concurrency policies arise).
(Okay, probably not twice as slow, since the cache will be hot, but still.)
Or if you "know" it's there call the version that throws an exception if it isn't.
Not having non-throwing versions of these methods (e.g. Java) means you lose the ability to signal intent and you have to do a lot of pointless catching of exceptions.
I am quite a fan of Rust's solution, with Result<T> types and various macros/methods which can do the commmon stuff for you
I agree having to write the same method a bunch of times is tedious but that is usually not the case. Most situations have clear intentions. It's much more likely in framework APIs where there are a bunch of non-task-specific operations.
I find the whole manual propagation of errors, even with a lot of helpers, to just be tedious boilerplate. Most of the time I literally don't care about the error -- I want to crash my app, log the error, and alert me and the user. I don't need to propagate the potentially limitless number of errors possible in any non-trivial application.
Optional<Womp> getWomp() {...}
Womp womp = getWomp().orElseThrow();
> A null value, a plain "error" object, or an error argument
In my mind, this just adds a lot more mental overhead while also being less informative than a simple exception. Plus you have to keep track of which method your current library has chosen to represent errors.
Note I am biased from using Python for a long time.
Any time you have user input, you can expect errors and so should have proper paths to handle file not found, invalid input, ... For those case, exceptions should not be used.
It's not a normal thing for code that needs that record that wasn't found.
> They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself.
They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error or your code wanders away from the root cause and fails later.
No offense, but I don't know where that idea comes from. Systems are checking for records all the time in ways where the absence of data doesn't necessitate throwing an exception.
For instance, a page, user, or piece of media on a website may have existed at one point but was since deleted, but still has a permalink floating around the net. Is it useful, in the case that someone clicks on such a link, to throw an exception when I can instead choose to render different page content when a record wasn't found? I'll never need a stack trace for that.
> They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
Why would I want a code path for expected behavior? I agree for cases like a failed connection, where the system is actually broken, but there isn't anything fundamentally broken about data absence.
> Also, you've been helpfully kicked onto the alternative execution path to handle that situation.
That's not only presumptuous, but if I wanted that to happen, I can do so myself.
> Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error.
Hmmm...
const record = store.findRecord(params.id);
if (record) {
render('show-record');
} else {
render('record-not-found');
}
or... try {
const record = store.findRecord(params.id);
render('show-record');
} catch(err) {
if (err.name === 'RECORD_NOT_FOUND') {
render('record-not-found');
} else {
throw err;
}
}
I'll let people decide which one is better. I personally prefer the first one. item = db.findItem(id)
assert(NotFoundError, item != null)
render('show-item', item)
upstream middleware: try {
res = await downstream()
} catch(e) {
if (e is NotFoundError)
render('not-found')
else
render('internal-error')
}
you can do this with other patterns for upstreaming errors like Result + Rust's handy short-circuiting, but your example doesn't demonstrate much. what about internal errors like your store throwing a database error, your first example doesn't even have a store capable of erroring? your comparison incomplete. and surely you don't do all error checking at every callsite no matter which pattern you use.Which is essentially GOTO 404
A horrible idea that I see often in clever frameworks disobeying encapsulation and reasonable control flow in favour of magic
On the one hand, it's terribly convenient when you're writing the code.
On the other hand, as clon says, it breaks encapsulation. Not necessarily, but I've rarely seen these style of Web frameworks used in a way that doesn't result in network communication concerns wormholing their way through the entire codebase. And it sometimes does do weird things with control flow. Again, not necessarily but in practice. For example, JAX-RS frameworks do the exception mapping off in some magic location that's outside the part of the call stack that you directly control, meaning that customizing the logic requires hacking some fairly complex special-purpose mechanisms into the framework.
I suspect that there's a way to achieve convenience without relying on spooky action at a distance. But one could be forgiven for looking at the way popular frameworks on popular platforms work and concluding that cleanliness must be the price of convenience.
That's JavaScript, right? That's probably not the best language to use to judge the concept of exceptions.
It's cleaner in languages like Java:
try {
Record record = store.findRecord(params.id);
render(record);
} catch(RecordNotFoundException e) {
render('record-not-found');
} catch (Exception e) {
render('unexpected-error');
}I like it. I wish that the concept of exceptions was closer to Panic because then we wouldn't be doing nearly as much flow control.
In the Rails world (where I live) looking top down, exceptions are handled via flow control or 500 pages. Anything that doesn't result in a 500 is effectively flow control.
While I agree with Rust's semantics, it generally isn't that bad to work around and knowing which is what.
Some things are essential to the page we are rendering, and if they are missing we should 404.
Some things are ancillary, and if they are missing we should render a stub in their place. E.g., saying that a comment is from "deleted user".
Exceptions are a good fit for the former, where we want to unify every failure. They are a poor choice for the other case, where we want to treat each failure special.
Which case a given fetch represents is a choice that should be driven by the fetcher.
They have a type, make it say MissingUserException and the special place can handle it in a special way. Ensure it is a subclass of some general exception type you also handle elsewhere and you're good.
Often people throw generic exceptions instead of using the type system.
I agree that a database API should not itself throw an error if an empty result set is found. It may throw an error if there is an SQL error, though, and really should. When one gets an empty result set where this should not happen, for instance because a row was just inserted and one is in a transaction, the user code could throw an exception instead.
Making exceptions optional is something that I have indeed done in the past because some uses of the code could benefit from exceptions while other uses did not so much.
The first example: - no stack trace - loosing the specifics of error to troubleshoot.
The second example: - explicitly discards the stack trace - explicitly discards the error specifics.
Hence, if this is really what you want. fine, but in a lot of cases you do want to see stack traces and the exact reasons (and meta data why an error was thrown).
And you are the one who knows best if something is an expected failure, so wrap that up in a fetch_one_or_none() — as a matter of fact, most DB interfaces I work with have an API of the kind.
Basically, exceptions are a decent way to keep what might need to be a separate control flow, well, separate. And you can choose at any time to "join them in". As a concept, they are quite like interrupts on the very low (hardware) level, so you can learn only one "tool" to deal with them.
(And yes, I can trivially find cases where I want errors to bubble up when a record is not found, eg. a required configuration record or similar)
And this is the problem with error checks: They're purely optional as far as the language is concerned, so we keep getting them wrong or forgetting things, even when we're being careful.
My response to that would be that if it isn't a normal thing for your code to encounter a record not being found, then you should handle that state in whatever you see fit, whether that's by passing down a value or object reflecting that error state or throwing your own exception(yes, I do think such a thing can be appropriate). I still don't favor the latter, but at least with throwing your own exception your application can catch it at a higher level and will be less likely to mistake it for another exception of the same class that should be handled differently. If you are using an SDK that throws an error for a "record not found" type situation, it would certainly work in your favor if your application really does benefit from it, but I think a lot of code doesn't actually call for such a thing.
But my point is that there's no reason I can think of that an exception can't be totally optional in these circumstances. I don't think it should be assumed that everyone wants an exception. There's a reason why we use conditional syntax like if-statements because, if used try-catch syntax as control flow for everything, the flow would be bass ackwards and hard to understand. In my opinion, the example situation I described is on the borderlands where an exception might make sense but probably doesn't actually make things better.
Every project is different, and people might choose to use exceptions a lot, which is fine for them. I've personally been better off not relying on the exception-based control flow taught to me when I learned Ruby, but that doesn't mean I would come into a project and remove everyone's exception handling.
That's basically limitation of the language. Null checks and result checking can basically be abstracted away with non-nullable types, option types, and result types and bind.
You only need to do match or check right at the edge, so they're not everywhere.
Like, handling a checked exception (Java) and a Google-cpp StatusOr<T> or a rust Result<T> provide extraordinary similar code. Unchecked exceptions provide a bit of extra dynamism, and languages without either are anti-user.
> It's not a normal thing for code that needs that record that wasn't found.
This is something of a religious dispute and there are arguments both ways.
But I think it’s important to note that an exception is vastly more expensive than a simple function call or return value. So I’d say you need a really good reason to use exceptions.
I agree with the GP that in general, “lookup failed” is not an exceptional error. If it always succeeds, why are you looking up something in the first place?
I agree with the original poster that exceptions do have their place, even though they’re expensive.
If you have the primary key of a database object that you know to be correct, and you try to retrieve that record but fail, that might legitimately be an exception (unless having records deleted from under you is a common occurrence in your app).
Kotlin gets this right for the most part (although I think it would be better if it had checked exceptions).
Maybe not Python? But Python is slow regardless.
But C++ does not provide a stack trace.
I’d simply add that most people greatly underestimate how expensive exceptions are, even in C++.
Which makes exception in C++ a very, very bad idea!
My reaction to 'the test fails because map::at threw an exception': stupid STL!!
A core dump would be so much easier to analyze.. Gdb's 'catch thow' is wonderful (well, it is after I fixed the part of our codebase which use exceptions as a control flow mechanism)
> A core dump would be so much easier to analyze..
Uncaught exceptions call terminate(), which by default calls abort(), which generates a core dump. So if you want a core dump, just avoid catching the exception.
Also works in C.
When a new exception object is created, the slowest operation is filling information about the stack trace. It is possible to override Exception's fillInStackTrace() with an empty implementation. In this case throwing exceptions with new exception objects, is only slightly slower than using one exception object (17ms vs 15ms for 1M throws).
Deeper stacks make difference even bigger. Adding 100 nested invocations to the stack slow downs the classic approach (a new exception object created just before invocation) to 9 seconds, while alternative approaches are not affected.
That said, I agree exceptions should be reserved for exceptional circumstances. Something like checking if a record exists or not should probably use something like Optional instead, unless you are in a situation where a record "should" exist but doesn't (which is itself an exceptional case).
In many popular programming languages, the way to check whether some value has a certain property is with an “if” statement. It would be very odd to replace every code path inside an “if” statement with exception control flow simply because that code path “needs” some condition to be met for it it to execute.
[0] https://www2.lib.uchicago.edu/keith/ocaml-class/pattern-matc...
[1] https://dev.to/babak/exhaustive-type-checking-with-typescrip...
obj = session.query(MyObject).one_or_none() # no exception if missing
obj = session.query(MyObject).one() # exception if missing
https://www.youtube.com/watch?v=AnZ0uTOerUI Unconditional Code by Michael Feathers
One solution to this problem is discussed toward the middle: giving data instead of asking for data avoids error conditions. Doing so pushes use of the data farther down the call tree and pulls acquisition up near the top, where we are closer to the user and thus able to more clearly decide what if anything to do about these corner cases.
Why call it then?
Unfortunately in static languages this leads to an unholy amount of boilerplate:
public Result<SomethingGood, SomethingBad> PerformUpdate(int goodThingId, UpdateForm form) { ... return new Result<SomethingGood, SomethingBad>(error); ... return new Result<SomethingGood, SomethingBad>(obj); }
The type annotations were hell. Sometimes there were three cases we wanted to consider. We went back to using exceptions, and are keeping a keen eye on the new C# features.
So ultimately - I agree with the author that throwing exceptions is fine, when things actually go wrong. It's also not that bad when things kinda went wrong, but sometimes the effort required to fix it isn't worth it.
public Result<SomethingGood, SomethingBad> PerformUpdate(int goodThingId, UpdateForm form) { ... return Err(error); ... return Ok(obj); }
which is considerably less boilerplate-y. When there are three cases you want to consider, that's no longer a sum type consisting of just success or failure - that's a different sum type. Maybe even represented as Result<ThreeCases, Error>.
F# is a lot better at using result types. You don't even need to specify the return type.
And if you're dealing with non-determinism, then even a pair of APIs which let you separate out existence-checking from fetches isn't enough; you just introduce a race.
I like how .net does it with methods returning bool and an out parameter. Pattern matching on a return value would be fine too. It only makes sense on methods where you reasonably do need to handle the error case. And a variant which triggers an error condition on failure should exist too, for simpler programs which don't need to handle such failures, merely log them.
I like this a lot since generic utility methods can have use cases where you sometimes want an exception thrown or simply an error code returned.
If I am working in Java and a checked exception is thrown, my method usually allows the caller to supply an Function<UnderlyingException, DomainException> so that the caller can specify that a domain exception should be thrown instead of having to try-catch the thing.
But I agree with the author of the article - Exceptions are the right design technique when the caller considers an error as a failure as opposed to an unusual but handlable situation. Messing with Result in a deep call-stack is painful.
They are trying to avoid returning null in this scenario.
Unfortunately, if the API is designed poorly you're stuck with it.
I can sort of understand why people hate checked exceptions but working without exceptions is just terrible.
There's no one-size-fits-all solution. In a lot of ways, the best response to "record not found" is that you get the same result as finding one, except with zero answers. That means your main line can process the same way -- unless that doesn't produce the result you need. In which case maybe you an if(null) somewhere, or a special fake record, or an exception.
At the same time before NULL, devs used to use "guard values", so NULL is really just a convenience.
for instance, just to illustrate what I'm saying:
let NULL = {} /* should every lib define its own null value? */
function GetOneRecord(){
let dbResult = queryRecordsFromDB();
if (dbResult.length === 0){
return NULL
}
return dbResult.records[0];
}
Optional types, are better, but one needs functional programming features for it to be really useful.What's people opinion on zero values by the way?
There are more clever ways to do this nowadays with monadic Result types, but the core idea of forcing a developer to handle error conditions is useful in API design.
Well, you have no idea in Java either, given that unchecked exceptions exist.
I understand the goal of checked exceptions, I just haven't found much value in them in practice, and given that nobody has copied them I don't think I'm alone. I'm sure it depends what area you're working in, but in my experience it's rare (not unknown, but rare) to be able to do much with e.g. an `IOException` at the call site, and naive approaches like automatic retry can easily be worse than crashing if those retries end up looping excessively and spamming a resource that was already struggling.
My personal preference is for non-exception mechanisms (Maybe/Option/Result objects, or C#'s TryXXX methods) to handle recoverable cases, and unchecked exceptions with very high-level catchers to convert the nonrecoverable ones into sensible diagnostics. YMMV, obviously.
The best motivation for unchecked exceptions are things like division by 0, or null pointer dereferences: these are things that result from programmers misusing the API, and are generally unrecoverable. What you want instead is some sort of "graceful crash"--for example, if you're a web server, display a 500 page and log the stack trace somewhere internally. Using a pretty distinct mechanism for driving these error cases (e.g., Rust's panic mechanism) I think works better for emphasizing the crash aspect here.
Of course, now you have the regular checked exceptions which are a fundamental part of the function type. It's not clear to me that the try/catch/throw model is necessarily the best model for propagating these exceptions, and there are several cases where the overhead involved in the (misleadingly named) zero-cost exception handling model are not appropriate for the model.
Although there is something to be said for easy upgrading of checked into unchecked exceptions. It's a relatively natural assertion that something can't fail for reasons the compiler doesn't understand (which means informing the programmer when and where it failed is very critical!). I've always been annoyed by Eclipse deciding that the default implementation of catch should be "ignore the exception" instead of "wrap it in a RuntimeException."
Testing is big this way, and I've had many 'discussions' with people who could demonstrate no experiences dealing with the consequences of their testing decisions two years on. Either they were gone or had externalized those consequences to others.
Getting multiple exceptions from multiple layers of the code only becomes problematic after the depth and breadth of the call tree has passed the point where a single developer can keep it all in their head.
I feel like encapsulation means that functions often should throw errors, because the small, one responsibility functions shouldn't have knowledge of control flow. For example, division. Anytime you divide, you might try to divide by 0. That's an exception. The / operator returns a number or throws an exception. Who would use a TryDivide function? What would it solve? The same error might be an exception in one scope, and expected in another.
I get that exceptions can be expensive, but they should be rare. Saving time on the pre-check for an exception might save more time than the rare exception. I would never throw an exception strictly for control flow, but avoid exceptions is a bad idea IMO.
But the line is very blurry.
Errors can be of two kinds: recoverable and unrecoverable.
Recoverable should be handled by checked exceptions and unrecoverable errors should be handled by runtime exceptions.
There are scenarios where it is and scenarios where it is not, it's fairly common for APIs to present both throwing and non-throwing variants (many languages stdlibs, for instance, provide both mechanisms for associative array lookups), and of course in any language with exception handling you can convert either to the other according to the needs of the consuming process at the point where you interface just by wrapping the finction in one which either throws or swallows the appropriate exceptions, depending on which direction you are converting.
This is the thing. This is the root of the thing. Let developers decide what's right for themselves. There are too many "me-too" developers writing terrible code according to terrible guidelines and terrible "best practices" (lol) and few people are actually thinking about what they're doing.
I agree with everything in your comment. Every single thing, and I wish more people understood your (our?) point of view.
It sounds like it means “code that is unsafe for a particular reason”; name the reason and leave developers capable of criticising code that is unsafe for other reasons, too.
Unchecked Exception throwing code and stringly typed code are both unsafe. The world is better if we can call spades spades, instead of pitchfork style digging implements, because you want to protect playing card manufacturers.
Perhaps, but if we had come up with a new word, you or someone else would complain that we had done that instead of using a word that people already "understood". :-)
English is a language full of elision, and there's nothing inherently wrong with using one word for closely related meanings. When being precise, Rust uses the term "memory unsafety", but we humans usually shorten that to just "unsafety" because we are lazy.
My point was merely to state that a function throwing an exception [1] doesn't introduce memory unsafety.
---
[1] Notably, we call this "panicking" in Rust-speak; does the fact that we call it something different from "exceptions" confuse people? Probably at least one.
The language provides a way of kinda-sorta recovering from an everything-goes-wrong situation only to be nice to you, and not because that's a thing that's generally possible. You can't always recover from a segfault, or a buffer overrun, or the rest of the kind of things that cause panicking, even if you know that they're theoretically possible in advance, because if you could you would've fixed them already. The best you can do is make your web server return a 500, send logs, ditch its cache, restart in a sane state and hope for the best. You're not recovering that half-finished task, because the programmer can't reason about the program state in a situation where their code has been shown to be wrong.
Panics are only for exceptional failure cases. If this confuses people, it's (probably) because they only thought they understood; they needed confusing.
If you're talking about Rust, I can't comment directly because I haven't used Rust, but I've totally used Go's panic / recover to implement "return" and "break loop" in a language interpreter.
I should really rewrite that thing to do something more sane like bundle up a continuation state to return to, but I was feeling lazy. ;)
If you've got very tightly controlled inputs, it's not unreasonable to expect tightly-bound results (like the last example in the article).
If you're writing intermediary code between unsafe inputs and other libraries, there might be a reasonable boundary to catch all unknown errors and wrap as a failure.
In PhotoStructure's case, it's an easy solution: I simply don't import the given file. There may be a similar reasonable boundary in your problem space.
... but if one is doing that, one should do it very explicitly. I prefer a codebase that separates regular flow of operation from the "catch-all" or "panic-recover" loops that indicate "The buck stops here for catastrophic failure."
I never understood the hatred checked exceptions received especially from the younger crowd. I still write Java at work and I still use checked exceptions whenever they indicate an error condition that must not be ignored by the client code. Many new to the project developers hate me for it initially because they can't just "roll out a feature" without being forced to take care of the corner cases but over time they love the discipline that's imposed on them by checked exceptions.
Ergonomics are important. Chaining calls to functions that return a monadic Result/Either type is easier and neater.
Midori nicely separated panics (programmer or system errors) from exceptions. Panics _could not be caught_ - they crashed the process. (Processes were therefore really cheap in Midori). Java, on the other hand, put common programmer errors into the exception category in order to make it easier to work with common code (e. g. `IndexOutOfBounds` should be checked or impossible, but arrays are so common, they decided not to require it and they needed to be backwards compatible so they couldn't make array access return a `Result`-style object).
That said, using checked exceptions well is a great aid to making the code base understandable!
* https://ziglang.org/documentation/master/#Error-Return-Trace... * https://ziglang.org/#A-fresh-take-on-error-handling
I advocate for junior devs to start with the assumption that a new exception should be checked and only consider making it unchecked in specific circumstances.
In Java, a function may throw a long list of checked exceptions, and these lists tend to grow to inconvenient sizes in larger programs. For example, if foo() calls bar() and baz(), which each throw 3 different exception types, then now foo() might throw 6 different exception types. In contrast in Rust, each function can only return 1 error type. If a function needs to represent several different types of internal errors, then the crate that it's in needs to define an error enum type with a variant for each of those. (Either that, or the function can use a generic wrapper error that can contain anything. This is less common in library code but pretty common in application code.) This shifts a lot of work from library callers to library authors, which is a good thing.
Someone with more Java experience will need to correct me here: I think it's fairly common to bulldoze all that complexity by declaring a function that just "throws Exception". That saves you from writing out N different types (and more importantly, from changing every transitive caller when a low level library introduces a new exception type). But it kind of defeats the purpose of checked exceptions, by throwing away all the info they provide. It's a shame that you have this "all or nothing" choice when it comes to exceptions and how much complexity you want to deal with. In contrast in Rust, wrapper errors can define automatic "From" conversions from the lower level error types they wrap, and the standard `?` operator automatically applies those conversions. That means that in many cases, a low level library adding a new error variant might not require any changes in its callers at all. The new information is there for callers who want to look for it, but existing abstractions around the error type generally just keep working.
On the other hand, you're also free to catch and process some and only propagate a subset outside of the function or even wrap the ones you want to process and throw a different exception type from within your function that wraps the existing ones. It's not mandated but pretty idiomatic in Java that any Exception type you define should be able to accept a different exception as its 'cause' in your exception's constructor signature.
The important thing is for the API designer to think about the abstractions, i.e which types of errors should be part of the API and which errors are just implementation details that may change.
If the API designer is too lazy to put enough thought into that then the result will always be what you are ascribing to Java.
I think about this when I think about GraphQL. I like GraphQL, I really do, but GraphQL's N+1 problem is why I don't recommend it. The easy thing is to hammer the heck out of your database, the hard things is to parse the GraphQL request and correctly transform it into a SQL statement. I just don't trust everyone on the dev team to not be lazy.
> I think it's fairly common to bulldoze all that complexity by declaring a function that just "throws Exception".
These are both solved by the same approach: foo should be wrapping those underlying exceptions under its own domain, not just passing them along. This has the added bonus that the further up the call stack you go, generally the more context you have about the operation. So you also get to set a new message that explains that context. So foo should only throw FooException, which might be wrapping an underlying BarException, which might be wrapping an underlying BazException. You get a full accounting of what happened, and how each layer interpreted it.
For example: I invoke the user preferences subsystem to give me the user's foo setting. User preferences can be in a flat file or an embedded database. If I don't wrap exceptions, now my user preference system has to expose that implementation detail by exposing both file and database exception types. Instead I should wrap both of them in a UserPreferenceLoadingException type or whatever makes sense.
I worked with Java professionally for several years and in a sense I feel the same: Checked exceptions are not as bad as they are often portrayed.
At the same time they are certainly not as nice as text book examples make them seem to be. For me the biggest downside always has been that their types are part of the method signature and therefore whenever you change the exception type of one class you more often than not end up refactoring half of your other classes too.
The tension between needing to communicate the actual reason for a failure (which violates abstraction) and preserving the abstraction (which demands shoehorning error states into some kind of polymorphic receptacle) is at the heart of the problem with checked exceptions.
If you try and pass through the failure mode, then new implementations cause new failure modes, which means methods grow new exceptions, which in turn break downstream dependencies, because adding a new exception to the throws clause is a breaking change.
If you try and abstract away the differences in failure mode (e.g. with a specific module exception), then you fill the code with boilerplate wrappers, need to translate exceptions at module boundaries, and (in the worst case) invent taxonomies into which all future implementations must awkwardly categorize their failure modes.
Specific to Java, there's another bug. The type system can express sum types only for concrete exceptions and not for generics. That means that if you parameterize code with other code (e.g. with a lambda or callback), the argument code can only throw pre-defined or runtime exceptions; there's no way to declare, in the type system, the transitive set of exceptions across control flow when the set of exceptions is determined by the argument. E.g. if you have a generic map(f) method, you can't generically declare that map() throws whatever f() throws without forcing f() to only throw a single checked exception - generic type arguments can't be the sum types that Java permits in throws and catch clauses.
All this is costly. Meanwhile, in most actual applications, exception handling is extremely rare; exceptions are almost always propagated up to a top-level handler and logged. When exceptions are handled, it's usually near the leaves of the control flow graph, where the application interacts with other systems, like the network or the file system; these inherently non-deterministic failures often need explicit management. So the work to propagate the exception types throughout the control flow graph is pointless.
And it's just super clunky, but the devs who came up with it just refuse any suggestions to have expressive exceptions (that's what exceptions messages are for). It's impossible to actually know what could go wrong when calling any method.
Here's a random rant online that ends up pitching the more common solution of unchecked exceptions + exception rewrapping: https://phauer.com/2015/checked-exceptions-are-evil/. And of course, in Rust, the rewrapping is basically forced upon you making for a really nice error-handling ecosystem.
I'd say not "younger crowd" but rather developers who have never dealt with a workplace or situation where he/she is demanded to take error-handling (and recovery case) seriously. Different workplaces have different accountability when it comes to code-quality (including error handling and i18n).
Java is more prevalent at some time in the past and it has built an amazing libraries of good/best practices while Python/PHP/JavaScript/Ruby weren't exposed to that level of commercial engineering scale in the past so when the developers came from the latter group, they rebel a bit when it comes to exceptions. Keep in mind that Java was born to address commercial needs thus there is more accountability compare to OSS languages. Just different mindset, different goals hence leading to different design decision.
Can you expand a bit on how you do this internationalization? I've got a low-priority issue [1] to investigate how to integrate SNAFU and Fluent to get i18n for error messages, but knowing how someone does it today would be super helpful!
Internationalization of error message? I didn't know that was a thing!
I'm curious about that, do you have some examples I could check?
Outside of the FP community where people care less about purity - the criticism is more that exceptions are hidden, they're all-or-nothing (either any function can throw, or no functions can, and not all code paths have runtime errors that you care about), and of course the overhead.
I also really disagree with the idea that result types have more boilerplate than exceptions. That just depends on the paradigms of the language you use, anyone can add sugar to make either easier/harder to read.
The questionmark operator, in particular, is what sold me on the possibility that this type of programming can be concise and ergonomic.
https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...
If you _can_ operate on those like collections, Rust is a much more fun language than I initially gave it credit for!
That's your problem right there. "Invisible control flow", as Joe Duffy puts it.
http://joeduffyblog.com/2016/02/07/the-error-model/#unchecke...
What's needed are two distinct error mechanisms: panic/abandonment for unrecoverable errors (which can happen at any time), and then for recoverable errors, either checked exceptions in some flavor (including Swift and Midori's untyped `throws`) or Result types.
Once you eliminate invisible control flow, you're no longer "better off using Exceptions".
We shouldn't expect downstream to wrap every array access which could be out of bounds in a "try" block. We shouldn't expect downstream to wrap every division operation in order to catch potential divide by zero errors. Those should be fatal errors, catchable only at a very coarse-grained level, because once they occur the program state is potentially compromised in a way which the programmer did not plan for.
The library author gets to decide whether a potential failure mode should expose a recovery API.
Old Ada programmer here. Example of reading bytes from a file... just keep reading bytes and don’t include logic for checking for EOF. Let the exception handler catch it where the file will be closed. Clean separation of code.
In Ada, every bock can have exception handlers at the bottom. No need for “try” syntax. Very clean.
What happens if the file is smaller than you anticipate?
That's why exceptions aren't used for flow control.
type Customer = { Id : string; Credit : decimal option }
let average (customers : Customer list) =
match customers with
| [] -> Error "list was empty"
| _ ->
customers
|> List.averageBy (fun c -> c.Credit.Value)
|> Ok
And argues that the F# programmer must conclude that all functions may throw because this function accesses the Option.Value without checking that it isn't None first and there's nothing in the type signature of the function to indicate that it may throw.Is that really how F# works? Similar code wouldn't compile in Rust. You have to handle both cases for Option<T> every time you want the value (or, yes, you can get the value by force, but if you do that, it's definitely NOT an accident and you're basically acknowledging that you want the whole program to crash if the value is None).
Anyway, I'm still in the Either/Result/Try camp. I even used Result/Try types in Java, Kotlin, and Swift (before they officially showed up in Swift and Kotlin).
I think the extra boiler plate in the middle of a function chain is absolutely worth it. I hate the idea that I need to actually read the source code of every function I call from library X just to see if it will throw an exception on me.
Swift has a pretty good compromise, IMO. A function's signature must indicate that it throws, but it doesn't have any type indicated. At least I know when I need to investigate a function further!
Normally you chain option operations with either binds, or you do a match(Which is exhaustive).
Directly accessing the value is normally frowned upon.
This guy either needed to make the option type go away, or filter out the None and pass that into average. List.choose could be used for this
let average (customers : Customer list) =
match customers with
| [] -> Error "list was empty"
| _ ->
customers
|> List.choose(fun c -> c.Credit) # Would remove the option type. Any nones would be filtered out the list
|> List.average
|> Ok
This is the best I could think off without a computerhttps://github.com/dotnet/fsharp/blob/master/src/fsharp/FSha...
You could instead write it as the following to handle that.
let average (customers : Customer list) =
customers
|> List.choose (fun c -> c.Credit)
|> function
| [] -> Error "no customers with credit"
| xs -> Ok (List.average xs)If it is an error that I want to handle explicitly, for example by showing a nice user message: use a result type. It is okay to use combinators in this case, but use them wisely.
1. Return values, like Either in functional languages
2. Exceptions
3. Error handlers, for example Lisp condition system. (Unfortunately, this option became less popular in programming languages, probably because Unix botched it.)
Now here lies the problem. Paradigm 2 is better than 1, because you don't have to handle the error at the caller (or at least, you don't have to unwrap the type). Paradigm 3 is better than 2, because the stack doesn't get unwounded when the error handler gets called - the error handler can choose to continue the original code. Finally, paradigm 1 is better than 3, because it is just so much simpler to set up and reason about.
So it turns out, you're better off using all of them! Although personally I believe exceptions are conceptually wrong, and in almost all cases, either 1 or 3 should be used. (Sadly, option 3 is not well supported in most languages.)
I think I only have some significant disagreement with one observation they've made, which is around runtime errors:
"It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. This is an awkward realization, which has to be addressed by catching as soon as possible."
I prefer to think of runtime errors as special case, and I appreciate Go's approach here: Go lacks any exception-handling more complex than panic and recover, and considers anything that is walking the panic / recover path to be an "exception handling failed" scenario. In other words, a correct program should have no runtime exceptions, and if one happens, it should explode as messily, noisily, and identifiably as allowed (and rare is the situation where the correct answer isn't "whole process dumps stacktrace and dies").
My suggestion for the case the author points to is to not catch that exception; if you take a runtime exception you don't anticipate, let it fly. It indicates you're "holding the API wrong" and you want to know about it ASAP, not try to catch around an unexpected failure mode. For expected failure modes, results and error types are often more comprehensible (though the issue of needing to address stacktraces still exists).
This is not always true. For example if you are writing high assurance software, and the error is recoverable, you really don’t want to crash. You want to catch it, log it, and continue/recover execution.
Of course, now you need to make sure that you’re not catching an exception that is not recoverable.
> I strongly believe that using result types as a general-purpose error handling mechanism for F# applications should be considered harmful. Exceptions should remain the dominant mechanism for error propagation when programming in the large. The F# language has been designed with exceptions in mind, and has achieved that goal very effectively.
If they were at least declared in the type so the compiler could give me a warning.
https://norswap.com/checked-exceptions/
> Summarized: people don't want checked exceptions because they are going to be abused by lazy programmers.
[1] http://normanmaurer.me/blog/2013/11/09/The-hidden-performanc...
enum Bool { True, False, FileNotFound };
λ :set -XTypeApplications
λ :t fmap @ (Either Error)
fmap @ (Either Error) :: (a -> b) -> Either Error a -> Either Error bFirst, if you are using a dynamic language, then exceptions make sense. You want to get there quickly, not handle every corner cases.
At the opposite spectrum, if you are writing rust, of course you want something very explicit and extremely aggressive to deal with errors.
They just serve different needs.
Also, exceptions don't have to be implicit, difficulties arise when the code you are using do not or cannot declare what can of exception can happen when you call it.
But if the language let you declare all the stuff you can raise, then there is not much difference.
Error handling should be done at the edges of systems rather than in the center. If you do that, it doesn't matter whether you use exceptions or error monads, you have a pure core that doesn't need to deal with error handling and a very slim area to catch errors. The mechanism you use for error handling, at that point, doesn't matter.
The problems of error handling often come from having it spread across the system and mixed with the logical flow.
I was recently telling someone about a little-known Java feature that I wish C would adopt. Goto is common in C exception handling code because the alternative is unworkably messy. Java has named code blocks that you can break out of, so they're not gogo, but they're also not exception abuse.
initBlock: {
if (fail) { break initBlock; }
doSomething();
} {
init()
}
const init = () => {
if (fail) return
doSomething()
}
(there's a few other cases i introduce functions for syntactic reasons in js - e.g. at the expense of a const funtion, i can convert a let to a const - but i have always found this improves the code on standard readability guides)I don't think exceptions should be used if they are caught and then retried or transformed. I don't think exceptions should be used in library code, except of course for unanticipated behavior such as hardware or network problems. In these cases it's better to use a result object, which better forces the library consumer to inspect what is being returned, rather than pass on what is returned and (maybe?) handle exceptions on errors. Ideally the library catches unanticipated errors and never throws, but those are edge cases that are probably not worth the investment by the library maintainer to handle. Development effort across the ecosystem is probably minimized when library consumers handle exceptional situations themselves, which they may have more insight into if their own infrastructure is causing the exception.
edit: grammer
Unfortunately, when you do eventually find yourself in a place where you have to care (which is the fate of any codebase that becomes large or complex enough), Python actively hinders making reliable code that is easy for strangers to modify without introducing subtle bugs.
foo = 0
if foh == 1: # oh no, I misspelled the variable
... many languages (including F#) will fail to compile the program because 'foh is uninitialized.' Python can't know if 'foh' is intended to be a global variable and so will execute the program and only determine while evaluating that line that 'foh' doesn't exist. This is especially insidious in error handling, where the error codepath isn't necessarily exercised; essentially, Python's flexibility implies that if your unit tests don't have 100% line coverage, you can't even know if your program is basically devoid of simple variable typos naming never-existing variables (a check most languages give you for free).
In a language with that feature, a rich ecosystem of exceptions is almost necessary, because every line of code could hide a runtime exception!
I haven't encountered a language with dynamic typing of the sort Python has that doesn't also have a robust runtime exception system, and it'd be interesting to see what that looks like. There's probably some old flavors of BASIC that fit that mold (i.e. variable declaration is not required and also the only thing it offers for exception handling is setting a label to GOTO if a runtime exception occurs).
I often fall into the "Python trap" (you'd think I would have learned by now!): "this is a tiny project, almost a script, and it's so much nicer and faster to code it in Python. And since it's so small, who cares about static typing? Surely this won't grow larger". A couple of months later: "oh, no!" [1]
[1] If you know the webcomic "webcomic name" by Alex Norris, it fits perfectly here.
I disagree. An exception can be faster than manually unwinding a set of nested scopes.
But most importantly, static type systems lose the ability to infer what's coming so these may turn out to be really pesky problems.
Personally I have no problem with your approach on the semantic level, but most compiler/interpreter developers consider all exceptional paths as cold as it gets: performance can be "interesting" if you take a lot of them.
Exceptions shoudl be exceptions. And python programmers often write a whole exception class model to define all business logic rules... Really use Enum for that!
class OurExceptions:
http_status_code = 500
class DatabaseRecordNotFoundError:
http_status_code = 404
Database queries are all written like: rows = db.select(...)
if not rows:
raise DatabaseRecordNotFoundError
and the top-level Flask error handler has code like: try:
return call_view()
except OurExceptions as exc:
return response(status_code=exc.http_status_code)
This is grossly oversimplified, but you get the idea. So, this means that we can write views like: def some_view(object_id):
obj = fetch_obj_from_db(object_id)
return {"found": obj.name}
If the object isn't found, the caller gets a 404 response without the person writing the view having to do a single thing. However, they can still handle the problem themselves if they really want to: def another_view(object_id):
try:
obj = fetch_obj_from_db(object_id)
except DatabaseRecordNotFoundError:
return {"error": "Not found. Try again later?"}, 404
return {"found": obj.name}
I absolutely love this coding style because exceptions are still being handled everywhere, but don't have to be explicitly dealt with deep inside a nested call stack. That lets us write very uncluttered, testable view code like: def change_password(userid, oldpass, newpass):
# This raises an exception if the user can't be found
user = get_user_by_id(userid)
# This raises an exception if the old password is wrong
verify_password(user, oldpass)
# This raises an exception if the DB couldn't be updated,
# perhaps because of a race condition with another request
update_password(user, newpass)
# By the time we get to this line, everything above has to have
# succeeded, with zero manual error checking inside this view
return {"result": "Password successfully updated."}This section of code is what i want to avoid:
user = service.getUser()
bill = billingService.getBill(user)
notificationService.message(user)
we use the same http client in all underlying service clients. let’s say you get to the end of this block and it results in SocketTimeoutException. How do you know what call did this? you would have to add to every line: .recoverWith({case t => new Exception(“billing exception”, t})
to propagate the context up. Either forces you to handle that well and write the context.a lot of it comes down to style choice because you could do it with exceptions, but i think this makes it easier not forget cases because the compiler will tell you
(I'm going to treat checked exceptions same as unchecked, because you don't _have to_ use checked or they can be trivially broken by using the Exception superclass)
At the beginning you often take shortcuts and ignore error handling in order to get the happy path to work. Once it is working, it is very easy to overlook some exceptions that should be handled, leading to unexpected runtime errors.
Using the Result monad (say in Rust), you can easily just add .unwrap() after any call that produces a Result to get the happy path working. After you're done with that, its easy to find all the places you took shortcuts and fix them.
To me, the ergonomics of a language is exactly this gap between how concise the "take all the shortcuts" code is and how easy it is to transform into production code where all the possible error states are properly accounted for.
Errors for trivial things (ie control flow) are an unfortunate abuse of try and catch; generally exceptions should only be thrown to users of an external API, not an internal one. If an exception is caught from the same layer of abstraction it was thrown from, it’s a very leaky abstraction — as opposed to, say, a regex parsing error, which ensures the abstraction isn’t implicitly failing.
A similar approach with type aliases (to be moved to value objects or structs with behaviour aka rich models) in Rust.
```rust type Amount = u128; type MoneySign = Option<Sign>; type Precision = u8; type CurrencyIsoCode = &'static str; type CurrencyName = &'static str; type Currency = (CurrencyIsoCode, CurrencyName); type Money = (Amount, MoneySign, Precision, Currency); type Balance = (Money); type ExpirationDate = &'static str; type MoneyWithdrawalError = &'static str;
enum MoneyWithdrawalResult { Success(Money), InsufficientFunds(Balance(Money)), CardExpired(ExpirationDate), UndisclosedFailure(MoneyWithdrawalError), } ```
That's because the whole expectation of exceptions/errors is built into the system unlike pretty much anywhere else.
Genuinely interested and i need to know.
Must be some kind of "goto catch block" internally when you throw an error. It has to stop the execution and jump somewhere, but that somewhere is set in the code where it catches the error.
If you're calling a function then that function throws an error, it has to prematurely exit to somewhere so there must be a stack of the locations of the catch blocks or something? That then unroll as you exit the try/catch blocks as well.
https://www.microsoft.com/en-us/research/wp-content/uploads/...
I've seen insane Java programs where tracking how many layers up Exceptions are caught was mind-twisting. And conversely, when everything was wrapped in a try/catch and then, essentially ignored.
The one parallel to (actual) exceptions is the author is arguing for a constrained subset of error states rather than a catch all Error type. This isn't a new idea. Java essentially does this with checked exceptions. C++ does this where you can declare what exceptions can be thrown (which you should basically never do).
So I did Java for years. Java had several Grand Experiments, one of which was checked exceptions. Despite it still having some fans I think the general consensus now is that checked exceptions were a Huge Mistake [tm] for many reasons (eg leaking implementation details, cluttering your API the whole way up).
One anti-pattern I see with exceptions is people using them for control flow. For example, dealing with a ParseException in Java when parsing numbers [1].
This of course falls into a religious argument about what an exception actually is.
For a few years at Google I wrote Google's flavor of C++, which I actually grew to really like. It is pervasive that any method in Google C++ returns a util::Status. This is much like Go error handling but (IMHO) better.
For one thing, it's a compiler error to ignore the result of a util::status (you can call .IgnoreResult() if you really want to ignore it). I think this is a much nicer and safer default.
You return things with a util::StatusOr templated union type that has the same semantics. There are even macros (that were somewhat controversial) to reduce boilerplate to do things like call a function that returns a StatusOr, assign the result to a variable if it's OK or return the error if it's not.
There are of course utility methods for adding context to a Status(Or) you're returning.
So all this came about because Google C++ strictly prohibits exceptions. This is a historic decision that's probably impossible to unwind at this point and honestly I don't think there's a strong motivation to change it.
IMHO exceptions are a false economy.
This is one of many things I like about Rust. Rust's enums are kind of the next evolutionary step for this. It's a compiler error not to deal with all options, there are constructs to reduce boilerplate and you can pass values.
[1]: https://stackoverflow.com/questions/8286678/parse-string-int...
Status(Or) is nice for certain types of errors, but now if you want to correctly deal with out-of-memory errors that means every single function that allocates anything on the heap now needs to return a Status(Or) in case the allocation fails. That error message then needs to be propagated through your entire library back to wherever it is that you can handle memory errors, which is likely somewhere at the root of the library. That now means that every single function in your entire library needs to return a Status(Or) and propagate it, which significantly bloats your codebase.
The macros you describe likely help with that somewhat, but then those macros are basically manual exceptions in that they just unroll the stack to where you don't call the macros anymore, but they require a bunch of extra manual effort and obfuscate your code somewhat. They are also slower in the common case (no memory errors), as you are now performing an extra check on every single function call in your entire library.
Meanwhile using exceptions together with smart pointers and smart locks basically makes it so that you can handle memory errors "for free" without having to bloat your codebase. When a memory error pops up, throw an exception and unroll back to where you can properly handle the memory error. The smart pointers/smart locks/destructors will take care of cleaning up everything. No need for any extra checks all over the code base for such a rare error.
Handling errors everywhere seems to always become ad-hoc exception handling. Oddly, the apologists call this "syntactic sugar for avoiding repetitive error handling", still can't wrap my head around that one.
The performance overhead comes from grabbing a stack trace and constructing an object that lives on the heap, and then unwinding the stack. (Some assembly experts can probably explain this overhead better than I can.). When your code handles an error condition that it anticipates, that overhead is wasted CPU cycles.
(This is why Exceptions aren't for flow control.)
What we really need are languages that differentiate better among success / error / exception. Go has panic / resume, and Java has compiler-enforced exception handling, unless it's a runtime exception.
What I want instead is something where I have optional and convenient syntax to handle lightweight errors; or the ability to automatically convert errors to exceptions if I don't handle them.
TLDR:
Think of the query to lookup a record by ID case. If the record isn't found, it's an error that doesn't have a stacktrace or an object on the heap. But, if my code doesn't handle the error, then it's a full exception with a stacktrace.
Result is not intended to be used for exceptions, but rather recoverable errors. There are a number of situations where I have used Result to recover from an exception that was expected, but it would be folly to try to handle all error paths (especially with runtime errors).
I think the author is trying to convey the point that we shouldn't use Result as a catch-all, but rather as a means of conveying possible known error paths for a function. There will always be the possible unknown error paths, which are handled by the runtime's exception handling/reporting.
I also frequently get stuck refactoring a lot of code other people have written because an exception tried to unwind across an ABI boundary, and nobody thought about how to handle that or what would happen. That ABI could be across a C module, which has no exceptions. Or perhaps across a C++ module built without exceptions, or using an incompatible unwinding mechanism. Cleanup gets skipped, undefined behavior gets invoked... it's a mess.
So, invariably, I write some catch(Exception) equivalents, if only to manually write the code to explode loudly and in a way condusive to debugging, instead of much later when the stack trace has been mulched through rethrows, or access violations from the UB, or ...
Now compare and contrast that to error codes, which I theoretically dislike, but swear by in practice.
Error codes are C ABI safe, any language can return them, set them, or store them. They're part of the method signature, neglected documentation be damned. There won't be any exotic failure modes where C++ destructors get skipped due to a setjmp style unwind. Even the absolute worst APIs usually have some kind of incomplete list about failure modes - a list of constants somewhere - and I have a fighting chance of deciding upon a sane local fallback (on top of breakpoint/logging/reporting) in the event of an undocumented/unexpected error code.
My biggest concern was always accidentally ignoring an error code, but every language at least has compiler extensions these days - to mark a function result as needing to be used - and a way of turning that warning into an error. Does this sometimes lead to a little extra error handling boilerplate? Yes. Does that even enter into the top 10 issues I have with error handling code? No.
I do get the occasional bug where ignoring an error code contributed to it's occurance, but those are a small price to pay compared to avoiding all the bugs arising from exception (mis)use I avoid.
> Because runtime errors are difficult to anticipate, I claim that using result types as a holistic replacement for error handling in an application is a leaky abstraction.
Yes this is true, but it's not a leaky abstraction. There's always the posibillity that shit hit the fan and something happened which was unexpected and indicates an unhealthy system in which case it will result in an unanticipated exception such as a RuntimeException.
This is ok, that's why every application has somewhere a global error handler, which can capture that unusual exception, log it and potentially terminate the application or put it in an unhealty state so that a higher level scheduler can replace or re-start the app. Possibly even raise some emergency alerts with developers via PagerDuty, etc.
However, it most certainly doesn't leak anything or makes the Result type less useful, because most application errors are anticipated due to a combination of user inputs or other external factors such as API calls and these can be perfectly handled in a more predictable way by using a Result type.
> It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. This is an awkward realization, which has to be addressed by catching as soon as possible.
Not really. There's nothing awkward about a runtime exception. Shit sometimes happen. And it's not true that it has to be handled as early as possible. As said before, you only have to deal with exceptions in a global error handler. If the error must be caught as early as possible, then it means that there is a possible plan B and a possible plan B can only exist if the error is anticipated. If it is anticipated then it should be returned in a Result type. So fundamentally exceptions are not awkward. When they happen there's exactly only one place where they need to get handled and the rest of the application code just works with Result types where errors are possibly known.
> In the majority of codebases using result types that I’ve been reviewing, people typically just end up re-implementing exception semantics on an ad-hoc basis. This can result in extremely noisy code...
So he has just worked with badly written code. If all the code is doing is bubbling up an error from the Result type then whats the point of returning that error? Return errors which are meaningful and where the calling code can deal with it immediately, otherwise don't bother.
> An important property of exceptions -which cannot be stressed enough- is that they are entities managed and understood by the underlying runtime, endowed with metadata critical to diagnosing bugs in complex systems. They can also be tracked and highlighted by tooling such as debuggers and profilers, providing invaluable insight when probing a large system. By lifting all of our error handling to passing result values, we are essentially discarding all that functionality.
Well as said before, the Result type is not to be logged or thrown or something. It's there so calling code can deal with it - implement some sort of plan B. If all the author wants is to always log the entire exception including stack trace for every error and not really deal with it then fair enough, just use exceptions everywhere, but that application will suck big time.
> That said, I strongly believe that using result types as a general-purpose error handling mechanism for F# applications should be considered harmful.
Functions are Input -> Output. If you don't like that as a "general" mechanism, and you prefer Input -> Output, Exception, {whatever} then use OOP and not FP. It almost seems like that the author just doesn't like the functional appraoch in functional programming, which is a weird point to make.
> Exceptions should remain the dominant mechanism for error propagation when programming in the large.
Based on which logic? That's such a generalisation that it's just plain wrong. Exceptions are try-catch error handling and there is no proof that try-catch is the ultimate error handling solution. Lots of new languages make an effort to exactly not do that, so where's the evidence?
Don't bother and do what?
And how do you know what the calling code can and cannot do?