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.
thisMightFail is asynchronous? For example, it may perform an asynchronous XHR to fetch the result data:
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.
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
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
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.
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
Next, we’ll look at Promises, and how they help to bring asynchronous programming back to a model that is simpler and more familiar.