Is try..catch really useful?
robertjwozniak.com
robertjwozniak.com
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.
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).
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.
Python is good but slightly too verbose because it needs to dump whole file paths.
> 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.
But you should never ever ignore(!) errors, hoping for the best (and I think try-catch encourages this, so I don't like it).
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.
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
}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.
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.
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.
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).
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.
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 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.
~ $ 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:4Exceptions 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().
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.
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.
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
vstry/catch:
try {
const p1 = await somePromise();
const p2 = await anotherPromiseFuncThatNeedsP1(p1);
// done
} catch(e) {
// log, throw, etc.
}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.
It's almost a strawman. Anyone would have written:
somePromise()
.then(anotherPromiseFuncThatNeedsP1)
.catch(e => whatever);- 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.
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.
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!)