Easier Error Handling Using Async/Await

Introduction

At work, someone asked if there were any better ways to handle errors when using the async/await syntax in JavaScript. They didn’t like their beautiful, short, and readable lines of code suddenly wrapped with try/catches. I’ve also been frustrated with a variety of the enthusiasm online the past couple years around async/await only to be shown code examples that completely ignore error handling.

Below is an easier way to handle errors using async/await by returning what’s known in Functional Programming as an Either. Mine isn’t as formal as the FP community’s “left right”. It’s just simple JavaScript Object that follows the Node callback naming convention somewhat.

tl;dr; First option is to create Promises that only call success with an Either, and Promise.resolve in the catch with an Either, or second option is to use a simple wrapper function.

What Problem Are We Solving?

The problem we’re trying to solve is taking this mostly readable code:

const go = async () => {
const data = await readFile('some.json');
const json = await parseJSON(data);
};

… and realizing you forgot the try/catch. The “readJSON” in this case is:

const readFile = () => Promise.reject(new Error('b00mz'));

Oh no! Let’s protect our function to handle that:

const go = async () => {
try {
const data = await readFile('some.json');
try {
const json = await parseJSON(data);
} catch (parseJSONError) {
console.error(`parseJSON failed: ${parseJSONError}`);
}
} catch (error) {
console.error(`readFile failed: ${error}`);
}
};

Gross. To make her gorgeous again, let’s go back to synchronous code first.

Did A Function Work Or Not?

The easiest way to ensure you don’t need try/catch using async/await in JavaScript is to create a function that never throws errors. Instead, it returns if it worked or not as well as the result or error. This is how Go works, and Lua via pcall.

Synchronous Example

Here’s a function that reads a json text file:

const readJSON = fileName => fs.readFileSync(filename).toString('utf8');

Note despite being a simple function, there are a variety of things that could go wrong:

  • The file might not exist or cannot be read for some reason
  • the encoding of the file is not UTF8 and we get garbage back
  • JSON.parse fails because of any of the above errors.

Two of those will throw an error, one of which we need to manually wrap with a try/catch.

No Errors, only Objects

Let’s instead write it like you would in Go or Lua. Instead of invoking a function and praying, we’re going to invoke it, and examine the response. The word “response” here is different than result. Like a Promise, it’s a container for a potential good result, or bad result.

const parseJSON = o => {
try { 
const data = JSON.parse(o);
return {ok: true, data};
} catch (error) {
return {ok: false, error};
}
};
const readJSON = o => {
try {
const string = fs.readFileSync(o).toString('utf8');
const result = parseJSON(string);
if(result.ok) {
return {ok: true, data: result.data};
} else {
return result;
}
} catch (error) {
return {ok: false, error}
}
};

Now each function attempts the operation, and if successful, reports it was successful as well as the data. If there was a failure, it reports that along with the error of what went wrong. You use it like:

const {ok, error, data} = readJSON('some.json');
if(ok) {
// use our data
} else {
console.error(`readJSON failed: ${error}`);
}

Prior Art

Node callbacks work in a similar manner. If a asynchronous call didn’t work, you’ll get an error as the first parameter in the callback. If it did work, that error will be undefined, and your data will be the 2nd parameter on down:

fs.readFile('some.json', (error, data) => {
if(err) {
console.error(`readFile failed: ${error}`);
return;
}
// use our data
});

Key Point

The key points to take away:

  • Don’t let your function throw errors
  • Return an Object with an ok Boolean: true if your function worked and has data, false if it does not and has an error
  • The data property is your data; it’s only there if ok is true
  • The error property is your error object; it’s only there if ok is false

Always Fulfilled Promises

The easiest way to ensure you never need try/catch for async/await is to never have your Promises reject. They always call success using the Eithers above:

const readFile = filename =>
new Promise(success => {
try {
const data = fs.readFileSync(filename).toString('utf8');
success({ok: true, data});
} catch (error) {
success({ok: false, error})
}
});

This has the side benefit of ensuring a bunch of these in Promise.all will never reject the Promise, leaving the status of the rest unknown.

Sadly, that’s too idealistic. You’re most likely using a variety of libraries that do not do that with Promises, or perhaps it’s your own code and there is a lot of it.

Jason Kaiser’s Sure Thing

Instead, you can use what my co-worker taught me using something called a “sureThing”. It’s a wrapper around a Promise to ensure it always succeeds.

const sureThing = promise =>
promise
.then(data => ({ok: true, data}))
.catch(error => Promise.resolve({ok: false, error}));

The secret sauce is the returning of a resolved Promise in the catch. This ensures the promise will never fire the .catch, or throw when it is used in async/await.

Putting It All Together

So, assuming our readFile and parseJSON are promises:

const readFile = filename =>
new Promise( (success, failure) => 
fs.readFile(filename, (error, data) =>
error ? success(data)
: failure(error)));
const parseJSON = o =>
new Promise( (success, failure) => {
try {
const result = JSON.parse(o);
success(result);
} catch(error) {
failure(error);
}
});

Using a sureThing, our above example can now be rewritten:

const go = async () => {
const readFileResult = await sureThing(readFile('some.json'));
if(readFileResult.ok) {
const {ok, error, data} = await sureThing(parseJSON(data));
if(ok) {
// use our data
} else {
return {ok, error};
}
} else {
return readFileResult;
}
};

Extremely imperative looking, giving control to the developer on what to do with those errors with no need for try/catch.

Conclusions

I hope you can see you can keep your async/await code try/catch free by creating functions that don’t throw errors. Using a simple wrapper function, you can use this against 3rd party code or other Promises that don’t follow this rule.

Now, some of you may take issue with my term “gorgeous” after seeing the conclusion. Yes, there is no try/catch, but the developer is still forced to deal with the errors in their control flow making it a nest of if/then statements. No errors to dig and find reasons for, cool, but… ugh. I agree. In the future, we’ll talk about how we can better compose those functions to be more readable. You can see it slightly in the readJSON function in the synchronous example composing the readFile and parseJSON into itself. For now, this’ll get the C# transplants and Go/Lua aficionados back on track.

Leave a Reply

Your email address will not be published. Required fields are marked *