Exceptions and try/catch
Exceptions and try/catch are an intuitive way to execute operations that may fail. They allow us to recover from the failure, or to let the failure propagate up the call stack to a caller by either not catching the exception, or explicitly re-throwing it.
Here’s a simple example:
In this case, getTheResult
handles the case where thisMightFail
does indeed fail and throws an Error
by catching the Error
and calling recoverFromFailure
(which could return some default result, for example). This works because thisMightFail
is synchronous.
Going Async
What if thisMightFail
is asynchronous? For example, it may perform an asynchronous XHR to fetch the result data:
Now it’s impossible to use try/catch, and we have to supply a callback and errback to handle the success and failure cases. That’s pretty common in Javascript applications, so no big deal, right? But wait, now getTheResult
also has to change:
At the very least, callback
(and possibly errback
, read on) must now be added to every function signature all the way back up to the caller who is ultimately interested in the result.
More Async
If recoverFromFailure
is also asynchronous, we have to add yet another level of callback nesting:
This also raises the question of what to do if recoverFromFailure
itself fails. When using synchronous try/catch, recoverFromFailure
could simply throw an Error
and it would propagate up to the code that called getTheResult
. To handle an asynchronous failure, we have to introduce another errback
, resulting in both callback
and errback
infiltrating every function signature from recoverFromFailure
all the way up to a caller who must ultimately supply them.
It may also mean that we have to check to see if callback and errback were actually provided, and if they might throw exceptions:
The code has gone from a simple try/catch to deeply nested callbacks, with callback
and errback
in every function signature, plus additional logic to check whether it’s safe to call them, and, ironically, two try/catch blocks to ensure that recoverFromFailure
can indeed recover from a failure.
And what about finally?
Imagine if we were also to introduce finally
into the mix—things would need to become even more complex. There are essentially two options, neither of which is as simple and elegant as the language-provided finally
clause. We could: 1) add an alwaysback
callback to all function signatures, with the accompanying checks to ensure it is safely callable, or 2) always write our callback/errback to handle errors internally, and be sure to invoke alwaysback
in all cases.
Summary
Using callbacks for asynchronous programming changes the basic programming model, creating the following situation:
- We can no longer use a simple call-and-return programming model
- We can no longer handle errors using try/catch/finally
- We must add callback and errback parameters to every function signature that might eventually lead to an asynchronous operation
We can do better. There is another model for asynchronous programming in Javascript that more closely resembles standard call-and-return, follows a model more like try/catch/finally, and doesn’t force us to add two callback parameters to a large number of functions.
Next, we’ll look at Promises, and how they help to bring asynchronous programming back to a model that is simpler and more familiar.