Simplifying async/await error handling

async and await are a pair of operators available in ES6, TypeScript 1.7 (when targeting ES6), or TypeScript 2.1 (when targeting ES3/5). Used together, these operators vastly simplify asynchronous programming when using Promises.

Consider the following asynchronous code to retrieve some data, then perform processing on said data:

retrieve().then((data) => {  
    processData(data).then((results) => {
        // Display results
    });
});

Using async/await, we can rewrite the above in a flat structure without the function nesting:

let data = await retrieve();  
let processedData = await processData(data);  
// Display results

Avoiding excessive nesting when using multiple asynchronous functions makes the code easier to read.

However, there is still a paint point when handling promise rejections. Normally, we would use catch() to handle an error from a promise rejection:

retrieve().then((data) => {  
    processData(data).then((processedData) => {
        // Display results
    }).catch((error) => {
        // Handle retrieval error
    });
}).catch((error) => {
    // Handle processing error
});

When using await, a promise rejection will cause an error to be thrown. To handle this error, we need to wrap each call in a try/catch block:

let data = null;

try {  
    data = await retrieve();
}
catch (error) { /* Handle retrieval error */ }

let processedData = null;

try {  
    processedData = processData(data);
}
catch (error) { /* Handle processing error */ }  

Although we don't have deep nesting here, we still have a problem; at each location that we want to handle errors, we must wrap the call with try/catch blocks. This leads to declaring variables outside the blocks, so they aren't scoped away. Both these points are undesirable.

To avoid this and keep these calls as flat as possible, I built a small helper method called on(). Using my helper method, the above code can now be written as follows:

let retrieveResult = await on(retrieveData());

if (retrieveResult.error) {  
   // Handle retrieval error
}

let data = retrieveResult.data;

let processResult = await on(processData(data));

if (processResult.error) {  
    // Handle processing error
}

let processedData = processResult.data;  

The on() helper function wraps the original promise with a promise that ensures it is always resolved, even if the original promise is rejected.

In the case of a successful promise resolution, the result is returned via the data property.

In the case of a promise rejection, the rejection value is returned via the error property.

The helper function is available as a TypeScript source file in this gist on GitHub.

Author image
Northern California
A software geek who is into IoT devices, gaming, and Arcade restoration.