
You’re better off using Exceptions - mumblemumble
https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/
======
ravenstine
My problem with exceptions isn't so much exceptions themselves but the way
they're used.

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.

~~~
Seenso
> A record not being found is a normal thing!

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.

~~~
ravenstine
> It's not a normal thing for code that needs that record that wasn't found.

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.

~~~
cjfd
The code example is not convincing at all. Obviously a single if is much
cleaner than a single try/catch block but that is not the comparison. The if
needs to be replicated N times for every N things that could fail and that are
calling each other in a potentially deep call hierarchy. The try/catch block
only needs to occur once at a level that is above all of the N things that
might fail.

~~~
dllthomas
In my experience, on the web there are two different kinds of things we might
be fetching.

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.

~~~
AstralStorm
Why are they a poor choice for the latter?

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.

~~~
dllthomas
In the latter case you will often want to pass "data or the fact the data was
missing" along to other functions. With a result value (of whatever form)
that's trivial. With exceptions, it's certainly plenty doable but it's messy.

------
pat2man
When I moved to Rust the constant error wrapping or converting annoyed me. But
after using it for a couple of years it turned out to be a huge blessing.
Quite often I need to know exactly which error messages will be thrown so that
I can do things like internationalization. While exceptions are quite
convenient for prototyping for production I’m now firmly in the typed errors
camp.

~~~
abraxas
I don't know Rust at all but what you're talking about sounds equivalent to
(much maligned) Java's checked exceptions system?

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.

~~~
oconnor663
I haven't written very much Java, but here are some differences between Java
checked exceptions and Rust errors as I understand them:

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.

~~~
fauigerzigerk
I don't think this is a real difference. You can make the exact same API
design mistakes regardless of which error delivery mechanism you use.

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.

~~~
oehpr
There is always an onus on an API designer creating an API to express it well.
However I think the difference between good vs bad language (and a good vs bad
API come to think of it) is that it makes doing the _right_ thing easy (lazy),
and the _wrong_ thing hard.

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.

~~~
fauigerzigerk
I agree with the principle, but in this case it seems very easy to do the
_right_ thing in either language once you have done the hard work of designing
a good API. The same goes for doing the _wrong_ thing.

------
unlinked_dll
> Such criticisms typically revolve around scoping considerations, exceptions-
> as-control-flow abuse or even the assertion that exceptions are really just
> a type safe version of goto.

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.

~~~
paulgb
> 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...](https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-
panics/the-question-mark-operator-for-easier-error-handling.html)

~~~
chaorace
I've not used Rust before, but this looks a lot like monads! Can I map/flatmap
over these or do I have to unpack them later?

If you _can_ operate on those like collections, Rust is a much more fun
language than I initially gave it credit for!

~~~
paulgb
Yes, you can map over them, and it's great! I think they satisfy all of the
monad rules, but I'm forgetting what they all are at the moment.

------
rectang
> _It’s by such misadventure that the working F# programmer soon realizes that
> any function could still potentially throw._

That's your problem right there. "Invisible control flow", as Joe Duffy puts
it.

[http://joeduffyblog.com/2016/02/07/the-error-
model/#unchecke...](http://joeduffyblog.com/2016/02/07/the-error-
model/#unchecked-exceptions)

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".

~~~
goto11
But then the failed code which decides if the error is recoverable, not the
part of the code doing the recovering.

~~~
rectang
Yes!

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.

------
waynecochran
Exception != Error

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.

~~~
quietbritishjim
That is like C++ destructors, which are called deterministically when they go
out of scope (again with no "try" syntax). They can unlock mutexes, close
files, etc. But unlike what you describe, there is no need to put anything at
the end of the function: instead, the fact that you have instantiated an
object already guarantees that its destructor will be called later. This is
called RAII, short for "resource acquisition is initialisation".

~~~
waynecochran
Yes... RAII is handy, but it doesn’t save you from checking EOF logic in my
example.

~~~
safety-second
You could use a File class that throws on EOF, but that would be a really bad
idea for C++ since throw/catch are very expensive.

~~~
quietbritishjim
Exceptions are expensive compared to a simple function call, but I imagine
they're cheap compared to operations that read from disk. Certainly one
exception per file close is not going to hurt.

------
ragnese
In the article he posts a snippet of F# code:

    
    
        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!

~~~
UK-Al05
Thats not how f# works normally.

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 computer

~~~
vikestep
In support of the article, your code would still throw an exception if all the
customers in the list have missing credit.

[https://github.com/dotnet/fsharp/blob/master/src/fsharp/FSha...](https://github.com/dotnet/fsharp/blob/master/src/fsharp/FSharp.Core/list.fs#L627)

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)

~~~
UK-Al05
Indeed. I was remembering the behaviour of average off the top of my head.

------
meijer
My rule of thumb is: if it is the type of error that I want a stacktrace for:
use an exception. For example: programming errors, unhandled cases etc.

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.

------
js8
There are actually three error handling paradigms:

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.)

------
shadowgovt
eirik's making some very good points regarding the downsides to avoiding
exceptions (particularly in the notion that one loses stack trace details and
if one doesn't think about how debugging will be done without them, one is in
for a world of hurt).

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).

~~~
baby
> 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

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.

------
correct_horse
The title contrasts the last paragraph. I believe the author means this to be
true only in the CLR, F# specifically.

> 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.

------
ferzul
Exceptions in Haskell are unavoidable and awful. If you want to write code
that responds to the errors that could occur, even some of them, you basically
have to read the library source because they're completely undocumented in
many libraries - even in the standard library (try something exotic like, um,
reading a file). And that means you need to know your whole stack.

If they were at least declared in the type so the compiler could give me a
warning.

~~~
hopia
That does suck, does it commonly occur because of non-total functions in the
libraries or what?

------
norswap
I wrote something in that vein, in which I defend checked exceptions (in
contrast with unchecked exceptions):

[https://norswap.com/checked-exceptions/](https://norswap.com/checked-
exceptions/)

> Summarized: people don't want checked exceptions because they are going to
> be abused by lazy programmers.

~~~
petilon
There is also a long discussion on pros and cons of checked exceptions here:
[https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...](https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dlang.org)

------
haagen
In Java, you can pre-allocate static final Exception objects with a pre-filled
stack. These exceptions have much better performance characteristics [1] and
are used by Netty [2] for control flow purposes. Sometimes these are helpful
and the performance is good, but only when you don't need a full stacktrace or
to customize the error message.

[1] [http://normanmaurer.me/blog/2013/11/09/The-hidden-
performanc...](http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-
costs-of-instantiating-Throwables/)

[2] [https://github.com/netty/netty](https://github.com/netty/netty)

------
rahilb
Reminds me of this classic

enum Bool { True, False, FileNotFound };

~~~
gambler
What you really need in such cases is a pentastate boolean:

[https://crossdysfunctional.tumblr.com/search/pentastate](https://crossdysfunctional.tumblr.com/search/pentastate)

------
smegma2
Can't the rethrowing logic be expressed using `fmap`?

    
    
        λ :set -XTypeApplications 
        λ :t fmap @ (Either Error)
        fmap @ (Either Error) :: (a -> b) -> Either Error a -> Either Error b

------
BiteCode_dev
I don't think it should be "exception vs returned value".

First, 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.

------
michaelfeathers
Good article but it is tangential to a basic design issue.

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.

~~~
Barrin92
it's not trivial in every domain to distinguish between pure and non-pure
operations. If you're writing software for control systems, robotics, anything
that has a very tight, low-level coupling between programming logic and the
outside world it's harder or often not possible to separate concerns into some
sort of data -> logic pipeline that lends itself to this style of programming,
or it may come at the cost of performance.

~~~
michaelfeathers
It's a decent goal, though. For all else, there's Erlang.

------
dehrmann
> ...exceptions-as-control-flow abuse or even the assertion that exceptions
> are really just a type safe version of goto.

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();
        }

~~~
ferzul
In that case, in javascript, i always just write a function.

    
    
        {
          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)

------
jackcosgrove
I think it's fine to use exceptions as error handling if the stack can safely
be destroyed with that exception. As the article states, they provide a lot of
information to help debugging and are resilient to refactoring. An example of
safe stack destruction is a web service call that cannot complete because of
the error. You're going to return an error response regardless.

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.

------
reffaelwallen
I love using exceptions as control-flow, Python makes it very easy, and it's
really helpful.

edit: grammer

~~~
shadowgovt
Many of the problems that people who eschew exceptions are trying to avoid or
fix, Python simply shrugs and declares isn't a problem. If the software you're
writing agrees with that assertion (which it probably does if you're writing
Python with no static type annotations and no pass through mypy or a similar
tool to verify those type annotations are sound), you're fine.

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.

~~~
bibyte
Can you please elaborate on those problems ? Personally I believe exceptions
are the worst way to handle errors, except for all other ways.

~~~
shadowgovt
Python's soft static type rules and extremely flexible variable instantiation
unfortunately turns every line of code into a potential runtime error, because
the code can't know at compile time if a variable was introduced to the global
context that could bind to any given name. Typos are therefore deadly at
runtime in Python in a way that they fundamentally aren't in other languages;
if you try to write

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!_

~~~
bibyte
Maybe I am missing something but I think every dynamic typing system has this
problem. I can't see what this has to do with Python and exceptions in
general. It is not like there aren't statically typed languages with
exceptions. This comment is an excellent critique of dynamic typed systems but
it has nothing to do with exceptions.

~~~
shadowgovt
The top-thread post was referencing Python, so I'm speaking in the Python
domain specifically. Other dynamic languages have this problem also, and other
languages with stronger static type guarantees do support exceptions.

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).

~~~
bibyte
I guess I was too fixated on the exceptions part of your comment. You are
right that due to Python's dynamic nature every line can turn into a runtime
exception. Maybe it's because English is my second language but I couldn't
understand that in your first comment.

------
foolfoolz
I’ve pushed for Either[L,R] based coding without exceptions for a while and
met a lot of criticism along the way. I still think it has value over
exceptions but this post does bring up good points. The one about throwing the
stacktrace away is annoying, but with Future your stacktrace is useless
anyway. We have a system where every error has a unique id that looks like a
jira ticket number so you can find the source very quickly and avoid the
stringly typed error.

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

------
trumpeta
Here's my problem with exceptions:

(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.

------
yunruse
I personally feel that use of exceptions kinda depends on how compiled the
language is. With strongly-typed, compile-checked code, it’s only really bare
assertions for a test suite to build. Meanwhile in weakly-typed languages
without means for overloading, certain type-based errors have to be made to
ensure the signatures are met.

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.

------
Gepsens
So much confusion between Exceptions and Errors in this thread. Exception
abuse is for bad code that needs to return early, because they have no pattern
matching, like Java and JavaScript. Nice languages have error handling, think
Go, Elixir, Rust, Scala.

------
ronlobo
Interesting article, also very F# specific.

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), } ```

------
baby
I’d say this works in Erlang because it is part of the culture, but in java or
python what I’ve seen mostly is people forgetting to catch exceptions, or not
being able to pinpoint where exceptions are catched.

~~~
hopia
Erlang has a very different execution model compared to those other languages
you mentioned. In fact, Erlang's culture in many ways encouraged to program
less defensively than you would be expected to in other languages (ever heard
of "let it crash"?).

That's because the whole expectation of exceptions/errors is built into the
system unlike pretty much anywhere else.

------
dana321
What's exception handling actually doing internally?

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.

~~~
gnulinux
For gcc/clang C++, internally, it will unwind the stack until it finds a
handler. This way it doesn't have to do anything at the moment of setting a
handler (i.e. "try" in the underlying code), as a trade-off it's relatively
expensive to throw an exception. But note that C++ standard does not specify
this, so it's fair game for compiler to do anything. E.g. it can use long
jumps, or just dispatch different functions based on all possible exception
states (which produces very large binaries).

~~~
dana321
Thanks for that, nice to know!

------
alipang
It's almost a meme at this point, but the solution, of course, is algebraic
effects. Among other things they let us implement checked exceptions, but in a
sane way that's safe without requiring the annotation of every single
function.

[https://www.microsoft.com/en-us/research/wp-
content/uploads/...](https://www.microsoft.com/en-us/research/wp-
content/uploads/2016/08/algeff-tr-2016-v2.pdf)

------
LOL_Arch_Linux
While my favorite language (Erlang) has Exceptions, I really like the "let it
crash" model -- let the whole light-weight thread die and have the controlling
thread (a/k/a "supervisor") figure out what to do.

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.

------
cletus
I wonder how many commenters clicked on this link because this really has
nothing to do with "exceptions" (as in the things you raise or throw) at all.
So it's a fairly terrible title that is really arguing against a catch-all
Error case.

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...](https://stackoverflow.com/questions/8286678/parse-string-into-number)

~~~
mytherin
I think Java's flavor of exceptions are especially horrible, as you mentioned
a string->int parse failing is typically not exceptional at all but rather
expected. However, I do think there is a place for exceptions: exceptional
situations.

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.

------
luord
As someone who's been writing Go for too long, way too long, I agree.

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.

------
gwbas1c
The real problems with exceptions are that _they have a large performance
overhead,_ and that they differentiate poorly between anticipated errors and
truly exceptional situations.

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.

~~~
patrec
Exceptions don't _generally_ have a large performance overhead, they do in
C++. Ocaml exception handling for example was orders of magnitude faster last
I looked. And C++ itself is seeking to fix the super-slow design:
[http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2018/p070...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf).

------
holtalanm
i think there is a distinct difference between an exception, and a recoverable
error.

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.

------
MaulingMonkey
I theoretically like exceptions, and used to extol their virtues, but I've
come to hate them in practice. They're an endless source of fustration in that
the APIs you least expect will throw various unexpected exceptions for various
undocumented edge cases, which invariably leads to heisenbugs crashes in our
bug tracker that I need to go waste a few hours trying to repro and root cause
because I, foolishly, was a good boy who didn't spam catch(Exception)
everywhere (as that would also catch the null/index exceptions indicating real
bugs). Theoretically, forcing you to use checked exceptions would solve this,
but would defeat many of the supposed virtues of exceptions.

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.

------
bogdanoff_2
For the readAllText example, could it return Result<string, Exception>?

~~~
xeonoex
Yes, but you can still argue just throwing it might be better. This also
affects the stack trace, because, at least in C# (so probably F#) `throw ex;`
is different than `throw;`, which rethrows the exception as if it weren't
caught.

------
dustinmoris
I completely disagree with this article, as it makes a lot of claims which are
not true, uneducated or intentionally misrepresented to make a case for
exceptions:

> 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?

~~~
protomolecule
>Return errors which are meaningful and where the calling code can deal with
it immediately, otherwise don't bother.

Don't bother and do what?

And how do you know what the calling code can and cannot do?

