Skip to content

Brilliant Coding Blog

Be Brilliant

Programming for failure – asynchronous code

When writing asynchronous code, if you haven’t built your program to handle failure, then often, it may appear that the failure never even happened. Exception handling in asynchronous code is very different than in sequential code, because the failure has to be “woven” into the code itself. Let’s take a closer look at this!

Previously we talked about handling exceptions in sequential programs, that is programs which have a functional call stack that is in order based on how the code is written. In asynchronous programs the program execution is not as rigid since code execution is delegated to different processes, threads, etc. Because of this, it’s more difficult to reason about our exception handling separately. However, our goal is similar to that of sequential code: failures will happen, sometimes in unexpected ways and our code needs to handle these cases in a well designed way.

Let’s take a look at a simple asynchronous callback design pattern:


request.get(url, function(ex, res, body) {
    if (!err) {
        console.log('Success!')
    } else {
        console.log(ex.stack);
    }
});

We can see from this example the distinction between our error handling code and the main execution code is not distinct, we are using regular logic “if/else” statements and object truthiness to branch on the error instead of a specialized “try/catch” statements.

It’s important to note that the callback design pattern does not require asynchronous functions, but this is a commonly used pattern for making asynchronous function calls. The examples in this article use http requests because this is a common reason for needing to write asynchronous code. Often writing a program which needs to use the http protocol is often the first time many programmers encounter needing to write asynchronous code.

An important design consideration is to make sure the code doesn’t “block” the execution of code that otherwise could run concurrently. As a programmer you want to avoid making your program wait for external http requests to resolve. This type of issue, blocking execution, wastes cpu time and if it happens in loops or other iterative components, could drastically slow down the program.

Another important design approach is to let our exceptions “bubble up” from inner function calls to outer calls. This forms the basis of a “call stack” which is useful for logging as it gives a sequential view of the program execution that you can use to debug your program. However in the case of asychronous code, the inner call might appear much later in the logs and it can be very difficult to trace asycronous program execution in this way.

Let’s look at a nested example using our callback example:


request.get(url, function(err, res, body) {
    if (!err) {
        request.get(a_different_url, function(ex, res, body) {
            if (!err) {
                console.log('Success!')
            } else {
                console.log(ex.stack);
            }
        });
    } else {
        console.log(ex.stack);
    }
});

Hopefully the challenge is fairly obvious, we can’t push the inner exception up through the caller due to the fact that the caller already completed. So our program design choices are much more limited and in this case we are simply logging the exception and letting the program potentially crash in some unspecified way (ie, what happens when the inner call fails? does our model state become inconsistent? does the user sees a message? etc).

Generally speaking asynchronous code is not recoverable. Because you are designing your program to have concurrent execution, that implies that you are not going to “block” or wait for the inner function to complete and allow for some graceful error handler to determine the correct program flow.

Oh My! What can we do?
To be fair my example highlighted one of the worst design cases (often referred to as “callback hell”), there are quite a few better ways to design for asynchronous exception handling. For the rest of this article I’m going to give an overview of few of these options. However, keep in mind, this topic is fairly dense and there is a lot of information available on it. If you decide to dive deeply into it by asking “why?”, that can lead you to need to ponder the fundamentals of programming. I’ve included links to relevant external resources throughout the article.

    Here are the three options I’m going to cover:
  • Make the asynchronous code look like sequential code
  • Stop the flow of execution in your asynchronous code when exceptions happen
  • Make your execution flow into a chained set of functions
Promises

The examples below use Promises. Keep in mind that in code that uses Promises, often there is no literal construction of a “Promise”. The idea of a Promise is more of an abstract specification. Therefore the use of certain methods (usually “then”) implies the convention.

Option 1 – Async/await

I think this approach really is the simplest conceptually. Basically what we want is to have is our asynchronous code replicate the “look” of synchronous code. This is exactly what the async/await approach does. With this approach there is potentially some blocking going on here, however it is done “strategically”. In this case the strategy is enforce limited blocking inside of the specifically designated “async” function when the await is called. Otherwise the functions are able to execute concurrently. Let’s look at how our simple callback example changes with this design pattern:


const getData = async url => {
  try {
    const response = await fetch(url);
  } catch (ex) {
    console.log(ex);
  }
}

It’s important to note that this means that you need design this strategy into your code and denote the async and associated awaits. If these are implemented in a naive fashion, it can result in blocking your program’s execution and you might lose most of the benefits from concurrency.

Option 2 – Circuit breaker

In this option we start with a very simple premise, which is the asynchronous function should not worry about handling exceptions at all. Instead we are going to “proxy” all calls to execute through another “circuit breaker” function. The circuit breaker design handles exceptions in a very generic way which allows it to be used for virtually any purpose. The downside to this is the added complexity that gets introduced in this design. The internal design of the circuit breaker pattern is a state-machine that generally has three states (OPEN, CLOSED, HALF-OPEN) and a set of configuration parameters that can be used to change its behavior upon instantiation. For practical purposes this approach is best suited for solving complex problems in a highly reliable and robust way. Let’s take a look at what a “minimal” example could be:

   
// Function that we want to execute asynchronously
function get_url(url) {
    return request.get(url, function(ex, res, body) {
        console.log('Success!')
    });
}

// Setup circuit breaker instance for this function
const my_circuit_breaker = circuit_breaker(get_url)

// Execute it, and notice that it conveniently returns a "safe" promise
const getData = my_circuit_breaker(url).then(parse_response);

The added complexity in this approach means you will likely need a whole extra library that implements the circuit breaker design. This example assumes this is the case, I’ve included some links below to articles which discuss the specifics of that implementation. Also it’s important to note that in this example the circuit breaker will return “safely”. That is to say any errors will have been handled by the circuit breaker logic so you can then call “parse_response” assuming that “get_url” completed.

Option 3 – Chaining

The final option that I’m going to cover is comes in many different “flavors” so let’s start with the basic premise. Back from the beginning of this post we mentioned that by their nature asynchronous functions cannot be recovered. So instead of building one big function, we can decompose our function into smaller ones and “chain” them together. This approach allows us to handle exceptions “between” each function in the chain. Let’s look at a code example (using the “Promise/rejection” naming convention):


function get_url() {
    // For this example I'm using the fetch() API because it uses Promises.
    return fetch(url).then(response => {
        if (response.headers.get('content-type') != 'application/json') {
            throw new ContentTypeError();
        }
        // We return a new "chained" Promise.
        return response.json();
    });
}

get_url.catch(ex) => {
    if (exception instanceof ContentTypeError) {
        console.log('Exception - The response is not JSON')
    } else {
        console.log(ex);
    }
}
const getData = get_url().then();

With the chaining option, we have clearly moved away from a syntax that is representative of exception handling in sequential code. In fact the code itself more closely resembles the original callback example we started with. Notice how we have added a “catch”, but that it does not handle the exception but rather allows us to define an exception handler function (more correctly called a “rejection” since “exception” is more of sequential code terminology). Also the “try” part that we use in sequential code becomes implicit in the Promise itself due to the nature of asynchronous functions.

It’s also important to note that Promises used in the example are a common idiom in Javascript but the chaining behavior can be implemented in other ways too. There are other approaches such as: “Futures”, “Results”, “Either”, or simple functional composition. These other approaches make different trade-offs so should be carefully considered during the design of the program.

Wrapping up

Programming for failure is all about being aware of the design choices that you are making about your program with regard to handling exceptions. Like many aspects of software development there are a wide range of choices available and tons of information on this topic to research further. In this article I wanted to just skim the surface and cover a few common options. Knowing what options are available and their respective trade-offs is crucial in order to have good software design and to ensuring your program handles failure correctly.

Programming for failure – asynchronous code