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 doTry(). Using my helper method (and object destructuring) the above code can now be written as follows:

let { data: data, error: retrieveError } = await doTry(() => retrieveData());

if (retrieveError) {  
   // Handle retrieval error
}

let { data: result, error: processError } = await doTry(() => processData(data));

if (processError) {  
    // Handle processing error
}

console.log("Processed data!", result);  

The doTry() 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.

Note: The doTry() function accepts a promise as well as a function that returns a promise. By passing a function that returns a promise, the doTry() function can ensure that any uncaught exceptions are also handled.

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.