Hacker News new | past | comments | ask | show | jobs | submit login
Is try..catch really useful? (robertjwozniak.com)
22 points by robertjwozniak on Sept 2, 2018 | hide | past | web | favorite | 44 comments



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.


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

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.


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.


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


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


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.


> 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

I hope all developers that are not completely incompetent agree with the sysadmins here.


Sure, then ask them "what happens if you run out of memory in the middle of the 'new user' function" and see what their answer is.


There is nothing wrong to "ignore" errors like out-of-memory, as long as you know the consequences. Terminating the application on fatal errors is part of error handling. That's fine.

But you should never ever ignore(!) errors, hoping for the best (and I think try-catch encourages this, so I don't like it).


That's the mess I'm dealing with today. Lots of secret suppression on dozens of cases. Once we started logging and stopped seeing obsolete responses, the data loses started coming in. That's raising a lot of concern now


Oh I forgot one. Should we have error checking ?

Management (no error checking): everything looks good !

Management (error checking): our developers SUCK ! They constantly get close to losing data or insist on the database having this "correctness" technobabbly thing. Idiots.


The transaction doesn’t complete and you’re back with the previous state. Modern software is often written in a way that kill -9 is the actual supposed way to terminate the process, to drive the point home that aborting at any point should be safe.


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


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.


Modern Node and most modern browsers will print a warning to the console when this happens. See my answer on Stack Overflow: https://stackoverflow.com/a/52061395/247696


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.


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.


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.


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.


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

Indeed.

Having witnessed a couple of disastrous mishaps because someone didn't properly check return codes in their C code, I'd argue it's even worse than that. You can't predict the myriad of wrong ways your System will behave after an ignored error because things that were implicitly taken for granted aren't true anymore. And those are the types of errors that come with a great cost since a) you might not be aware of them the next few months and b) they are usually not easy to find in an automated fashion.


Right. Not catching an error is as bad as magically catching and ignoring all errors, and for the same reason: The state of the system is clearly unknown. Specific actions must be taken in response to the error to recover. It's generally safer to kill the entire system than to keep chugging on after an error without a tailored error handler.


If you use async/await and the awaited promise throws, the exception will become a hard exception in the context of the async / await (i.e. disrupt the control flow and exit).


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


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.


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


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


You should be, however I've delt with subtle bugs caused by throwing errors across the RPC boundary. Those exceptions weren't logged in the sending side.

The solution in the recieving side was a try catch, which ate the "connection terminated" exception. And instead of a stack trace, I'm dealing with broken connections later on in a different RPC.


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


In C++?


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


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


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.


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


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


Put another way, monadic error handling really requires `do` notation.


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.


Your first example is pretty malformed. If you're promise-chaining why would you be trying to assign to variables in the outer scope? Why are you assigning to constants? Why are you calling somePromise a second time in the first then?

It's almost a strawman. Anyone would have written:

    somePromise()
      .then(anotherPromiseFuncThatNeedsP1)
      .catch(e => whatever);


- re: const assignments: you're correct. I did not compile this and the assignments should use let.

- Show me where you have access to p1 outside of your closure.

The idea is / was -- you need access to both p1 and p2 for some comparison. The first `somePromise` would have been more accurately written `someInitalPromise`

The point is that try/catch + async & await removes a lot of cognitive overhead. Before async/await I rarely used try/catch -- so it's easy to get things done without it, but the combination of the two aforementioned has made codebases I work on much easier to maintain.


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.


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


Well, I have to say I am completely unpersuaded.


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.


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.


Yes, error handling is really useful.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: