
Is try..catch really useful? - robertjwozniak
http://robertjwozniak.com/article/is-try-catch-useful
======
haney
I feel like this assumes that the errors that might be thrown by a section of
code are knowable. For some high level projects with poorly documented
dependencies interacting with third party services that are in constant flux
it’s nearly impossible to know everything that’s going to happen and
try..catch allows you to program defensibly when an assumption the programmer
made is invalidated.

------
mlmitch
I ended up making an account for this one. Exceptions are a section in one of
my favourite blog entries:

[http://blog.ploeh.dk/2015/04/13/less-is-more-language-
featur...](http://blog.ploeh.dk/2015/04/13/less-is-more-language-features/)

Their argument is that Exceptions are GOTOs in disguise and point to the use
of sum types.

I have found this to be good advice when using Java 8+. Composing Optional<?>,
Either<?, Exception>, or CompletableFuture<?> has provided sufficient tooling
for handling failure scenarios.

I also find that Exceptions slow down my reasoning about code, because they
are side effects - see functional programming.

~~~
emodendroket
The Try pattern is good but if you are using a whole bunch of
libraries/existing code that doesn't use it already it's just going to be
confusing to introduce it.

------
candiodari
Should we have error handling at all ?

Software engineers: no, too much work to think about every possible error case

Sysadmins (looking at above statement): NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO

Now in theory you could do error handling without try...catch. Golang
especially espouses that vision. However, like C before it, in practice Go
programs ignore pretty much every error case.

To that practical situation, I will say, try...catch is far, far superior. Why
? Because it terminates your application instead of running the program in a
way that erases the database. As an additional bonus try...catch gives you
exactly what happened, and the entire stacktrace that mostly specifies what
all the parts of the application was doing (and if you know what you're doing
in Java/C++, including all values of all internal data structures. In Python
... well a little bit less but still substantial information).

~~~
notacoward
> try...catch is far, far superior. Why ? Because it terminates your
> application

Nope, that's try _without_ a catch. Every time you catch an exception you
prevent that (though of course you can turn around and terminate anyway).

> try...catch gives you exactly what happened

Depends on the implementation. Python's is actually one of the best this way.
For C++ what you say is simply not true. It's possible for an extremely
diligent programmer to define exceptions that capture more useful state, but
in practice almost no exception-intensive codebases provide as much
information as in a typical error-return based system.

~~~
kjeetgill
I find Java stack traces to be among the most readable. Exactly one line per
stack frame, and just enough information to hunt the source file/library down
the whole call stack.

Python is good but slightly too verbose because it needs to dump whole file
paths.

------
daxterspeed
.then().catch() is _basically_ just a fancy wrapper around try..catch, so I'm
not entirely sure what the example is supposed to show?

With async that code would be equivalent to something like:

    
    
      try {
        const response = await fetch(...);
        const data = await response.json();
    
        if (data.errors) {
          handleErrors(data.errors.code);
        } else {
          handleResponse(data.response);
        }
      } catch (error) {
        error; // handle error
      }

~~~
amelius
There's a difference: .then().catch() is worse because you can forget the
.catch() part, and your error will be discarded unless you set up a global
error handler (ugly because now the error will not bubble up properly). Not so
with try ... catch.

~~~
davnicwil
Isn't forgetting to chain a .catch() just the equivalent of completely
forgetting the 'try' block in async/await code though?

Seems like in both cases, the cause is the same: you weren't aware or forgot
that the async logic might throw, and so you don't write code to handle it.

~~~
Doxin
Sure, but the result is different. Missing out a try/catch will make your
program crash/log an error/whatnot when the error happens. missing out a
.catch() _might_ put an error in the console sometimes.

Fail fast is an important principle, but in my opinion fail hard is fairly
important too. If your program explodes every time a bug happens you're more
likely to fix them. If it merely doesn't do the thing it's supposed to it's
easy to miss and easier to ignore.

~~~
davnicwil
What you say is true and I don't disagree with it, but we're talking about
different levels of abstraction. Yes, at a lower level there is a technical
difference, but if you zoom out far enough, the result is not different, i.e.
in both cases the feature does not work.

I understand and agree with what you are saying about failing fast and hard
being more convenient to catch bugs, as an engineer, but then we get into what
is a bug and what can be ignored?

If the result is a mission critical feature breaking, you can't ignore that.
Inversely, if you can ignore it, then it isn't mission critical.

So my point was, at a higher level of abstraction they _are_ the same thing.
The cause is forgetting to handle an Error in your code, the effect is a
feature breaking.

Regarding that cause then, the OP was saying that it's easier to forget
chaining a .catch(), but that is only assuming that you'd actually remembered
to use a try in your async/await code, i.e. you are aware of the fact it might
throw. I can't think of many times where I have been aware something might
throw and forgotten a .catch(), but for some reason would have remembered to
wrap the call with a try, were it async/await code. I guess I am saying it's
unclear to me how try/catch helps one remember to handle Errors, over
.catch(). Seems like the syntax is not the issue, the issue is not being aware
or ignoring that the code might throw in the first place.

Obviously, this is all dependent on context and application and both
perspectives are 'right' for different scenarios.

~~~
Doxin
They are not the same, not even at a higher level. The default action is
different, ignoring errors by default is dangerous. Sure in many cases you'd
like to catch the error and ignore it, but that has to be a conscious decision
someone should have at some point thought about.

------
tomas789
First of all, the way of handling the errors really shouldn't depend on the
size of the project. Nobody is ever going to rewrite their error handling just
because the project grew up.

Second, and likely more important fact is that the server returning the
non-4xx status code is not quite an exception. It is just one step away from
your happy-path. The mechanism of exceptions should be used for things that
you don't expect. Mostly because of the cost they imply on the runtime (at
least for C++ and similar languages which do stack unwinding and not so much
for Swift, which treats the exceptions as just a different returned value with
almost zero runtime cost).

------
notacoward
Every complaint about a goto is valid for try..catch, except it's worse
because it's a _non local_ goto for which the destination is unknown at
compile time. Ugh. Yesterday I had to deal with a piece of code that relied on
a certain exception to do a particular kind of repair, but someone had added a
_different_ exception twelve layers down (and across an RPC boundary). This
was much harder to debug than it would have been with error returns, because
nothing in those dozen intermediate layers had a chance to see and log the new
exception. It just flew right by them, and when it arrived at its destination
there was no information about its provenance. Thus, I had to go through a
tedious process of instrumenting each layer to see how far it got before the
flow of control was yanked away from it, which took hours instead of the
minutes it would have taken to step through the same thing with error returns.

I know there are some techniques that could have made that experience better,
but this kind of crap seems to happen _every time_ I have to interact with a
codebase that relies on exceptions as a primary error-handling mechanism. That
means practically all C++ and Java where it's strongly idiomatic, and some
Python where it's more weakly so. Error returns might be more verbose, but
they're ultimately better for maintainability.

~~~
edflsafoiewq
From the CFG perspective, try..catch is strictly weaker than goto (you can't
build irreducible control flow with it for example). And, I mean, any return
is basically a non-local goto for which the destination is unknown at compile-
time.

Your story seems strange since I think one of the best features of exceptions
is that if you don't catch them, you always get a full stack trace which is a
great boon for debugging. If you bubble up error returns, the only contextual
information you get about where the error occurred is what every caller
attaches to the return. But I have had terrible trouble getting stack traces
(or types) from Python exceptions when they cross a process boundary so maybe
that's it.

(I have no strong opinion about whether exceptions are good or bad myself.)

~~~
notacoward
> you always get a full stack trace

You get a trace from where it was _caught_. Where was it _thrown_? Nobody
knows. To be fair, that's the same as you'd get with error returns, but at
least those dozen layers in between had a _chance_ to see and log the error.
Or in a callback-based system you do get a trace from where the error
originated.

The problem is that the act of throwing an exception destroys practically all
information other than what you put in the exception yourself, and very few
programmers implement exceptions in a way that provides as much information as
is already available with error returns. Exceptions should be _exceptional_ \-
rare events that don't get thrown or caught as part of normal operation. When
every possible outcome of calling a function is reflected as a different
exception, even if it's a fairly common case, that's when even simple control
flows turn into big piles of random jumps.

~~~
edflsafoiewq
No, you get a trace to where it was thrown.

~~~
anyfoo
In C++?

~~~
edflsafoiewq
Yes? Is there a difference in terminology here? Obviously you get a trace to
the point of the throw.

    
    
        ~ $ cat a.cxx
        int a() { throw "a"; }
        int b() { return a(); }
        int main() {
            b();
        }
        ~ $ clang++ -g a.cxx && gdb -ex r -ex bt a.out
        < ...snipped... >
        #6  0x0000555555554779 in a () at a.cxx:1
        #7  0x0000555555554789 in b () at a.cxx:2
        #8  0x000055555555479d in main () at a.cxx:4

~~~
anyfoo
There is no catch in your example. I thought the point was that in C++,
preserving the stack trace when having caught an exception is rather non-
trivial.

Exceptions that don't get caught usually allow you to get the stack trace
through a crash report/core dump/whatever, but then there's not much
difference between an uncaught exception and a plain abort().

~~~
edflsafoiewq
Yes, I said "if you don't catch them you always get a full stack trace".

The comparison was between returning error values and exceptions, not between
exceptions and abort. The difference is with abort is of course that once you
are alerted to the error by seeing it be uncaught, you can insert an
appropriate handler for it. A file-open function can throw on file not found
but I will not very happy if it aborts.

~~~
anyfoo
Ah, after reading the full thread, that is actually what you said, and I don't
really know what the point of the commenter you replied to was, now...

------
olingern
I would argue that reasoning about promise chains are difficult enough to
where an abstraction is desirable.

If you asked me to identify the most maintainable and easy to read of these
two pieces of code, I would say the answer is easy.

Promises:

    
    
      const p1, p2 = undefined;
    
      somePromise()
        .then((res) => {
          p1 = somePromise(res);
          return anotherPromiseFuncThatNeedsP1(p1);
        })
        .then((anotherPromiseRes) => {
          p2 = anotherPromiseRes;
          return p2;
        })
        .catch(e => {
          // log, throw etc. 
        });
      // done
    

vs

try/catch:

    
    
      try {
        const p1 = await somePromise();
        const p2 = await anotherPromiseFuncThatNeedsP1(p1);
        // done
      } catch(e) {
        // log, throw, etc.
      }

~~~
beojan
Put another way, monadic error handling _really_ requires `do` notation.

~~~
olingern
In general, I would agree.

I'm sure there are cases where Promises are still superior, but I think our
brains work easier in the x = y sort of notation.

------
gremlinsinc
I generally am picky on my try/catch implementations... I use it way more
often on the backend (PHP/laravel) than front. Though I will tap into
then/catch on async functions which I guess is built-in try/catches.

On the backend I mostly do try/catch when I have a bunch of things tied
together in a db transaction... If any part of the commit fails or anything
errors at all the whole thing will rollback. Esp. handy when doing multiple
relationship attaching in one go.

Also it comes in handy when dealing with API's and things, but for most
scenarios it's not needed too often. But I also don't test every little thing,
I believe moderation in all things including try, catch, testing. lol.

------
gumby
Unfortunately that web page cannot be fixed via "reader" mode in Safari.

But as I read it I felt there was an implicit "yes, but" running through it.
To step up a level:

\- unexpected problems occur \- you have to manage them. \- managing error
return codes is tedious, and consumes a lot of lines of code, which is an
opportunity for errors in and of itself.

Try/catch is a sign that something went wrong, but it allows you to manage
sequence points (via catch) to deal with them. With unwind-protect (typically
via automatic destructors in contemporary languages) most of the required
boilerplate is automated. Yes, there are corner cases (e.g. the reason for
C++'s make_shared) but there are _many_ more when you're tracking error
returns.

Now where I disagree right up front is that try/catch is _not_ a way to
"handle errors". It's a way to _deal with them_. We already have experience of
using signalling to _handle_ errors: the Common Lisp condition system allowed
you to catch an error and _restart_ the computation. It was a wonderful,
general mechanism (even more general than just this case described here) and
indeed, it was so general and so powerful that it also turned out to be an
opportunity for incomprehensible code. We all learned from that, which is why
nobody else has a facility for handling errors.

(BTW another great example of this kind of generality was method combinators.
Sounds great but in practice: no thanks!)

------
emodendroket
Well, I have to say I am completely unpersuaded.

------
Xyik
I think try catch is fine with some documentation or commenting around why the
developer chose to do so instead of specific handling for all the different
possible exceptions.

------
peterashford
try..catch means you don't have to have perfect knowledge of what errors may
be raised - which you simply do not have when dealing with external code. It
also means that you can guarantee that no exceptional case has occurred that
you have not handled. Either you explicitly deal with the issue or you have
the option to fail & log.

------
syntheticcdo
Yes, error handling is really useful.

