Async Programming Part 1: It's Messy

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:

  1. We can no longer use a simple call-and-return programming model
  2. We can no longer handle errors using try/catch/finally
  3. 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.