DEV Community

Sobio Darlington
Sobio Darlington

Posted on • Edited on

Better error handling with async/await

This article is intended to suggest a better way to handle errors when using async/await syntax. Prior knowledge of how promises work is important.

From Callback Hell to Promises

Callback Hell, also known as Pyramid of Doom, is an anti-pattern seen in code of programmers who are not wise in the ways of asynchronous programming. - Colin Toh

Callback hell makes your code drift to the right instead of downward due to multiple nesting of callback functions.

I wont go into details of what callback hell is, but I'll give an example of how it looks.

User profile example 1

// Code that reads from left to right 
// instead of top to bottom

let user;
let friendsOfUser;

getUser(userId, function(data) {
  user = data;

  getFriendsOfUser(userId, function(friends) {
    friendsOfUser = friends;

    getUsersPosts(userId, function(posts) {
      showUserProfilePage(user, friendsOfUser, posts, function() {
        // Do something here

      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Promises

Promises were introduced to the Javascript(ES6) language to handle asynchronous operations better without it turning into a callback hell.

The example below use promises to solve callback hell by using multiple chained .then calls instead of nesting callbacks.

User profile example 2

// A solution with promises

let user;
let friendsOfUser;

getUser().then(data => {
  user = data;

  return getFriendsOfUser(userId);
}).then(friends => {
  friendsOfUser = friends;

  return getUsersPosts(userId);
}).then(posts => {
  showUserProfilePage(user, friendsOfUser, posts);
}).catch(e => console.log(e));

Enter fullscreen mode Exit fullscreen mode

The solution with promise looks cleaner and more readable.

Promises with with async/await

Async/await is a special syntax to work with promises in a more concise way.
Adding async before any function turns the function into a promise.

All async functions return promises.

Example

// Arithmetic addition function
async function add(a, b) {
  return a + b;
}

// Usage: 
add(1, 3).then(result => console.log(result));

// Prints: 4
Enter fullscreen mode Exit fullscreen mode

Making the User profile example 2 look even better using async/await

User profile example 3

async function userProfile() {
  let user = await getUser();
  let friendsOfUser = await getFriendsOfUser(userId);
  let posts = await getUsersPosts(userId);

  showUserProfilePage(user, friendsOfUser, posts);
}
Enter fullscreen mode Exit fullscreen mode

Wait! there's a problem

If theres a promise rejection in any of the request in User profile example 3, Unhandled promise rejection exception will be thrown.

Before now Promise rejections didn't throw errors. Promises with unhandled rejections used to fail silently, which could make debugging a nightmare.

Thank goodness promises now throws when rejected.

  • Google Chrome throws: VM664:1 Uncaught (in promise) Error

  • Node will throw something like: (node:4796) UnhandledPromiseRejectionWarning: Unhandled promise rejection (r ejection id: 1): Error: spawn cmd ENOENT
    [1] (node:4796) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

No promise should be left uncaught. - Javascript

Notice the .catch method in User profile example 2.
Without the .catch block Javascript will throw Unhandled promise rejection error when a promise is rejected.

Solving this issue in User profile example 3 is easy. Unhandled promise rejection error can be prevented by wrapping await operations in a try...catch block:

User profile example 4

async function userProfile() {
  try {
    let user = await getUser();
    let friendsOfUser = await getFriendsOfUser(userId);
    let posts = await getUsersPosts(userId);

    showUserProfilePage(user, friendsOfUser, posts);
  } catch(e) {
    console.log(e);
  }
}

Enter fullscreen mode Exit fullscreen mode

Problem solved!

...But error handling could be improved

How do you know with error is from which async request?

We can call a .catch method on the async requests to handle errors.

User profile example 5

let user = await getUser().catch(e => console.log('Error: ', e.message));

let friendsOfUser = await getFriendsOfUser(userId).catch(e => console.log('Error: ', e.message));

let posts = await getUsersPosts(userId).catch(e => console.log('Error: ', e.message));

showUserProfilePage(user, friendsOfUser, posts);
Enter fullscreen mode Exit fullscreen mode

The solution above will handle individual errors from the requests, but its a mix of patterns. There should be a cleaner way to use async/await without using .catch method (Well, you could if you don't mind).

Here's my solution to a better async/await error handling

User profile example 6

/**
 * @description ### Returns Go / Lua like responses(data, err) 
 * when used with await
 *
 * - Example response [ data, undefined ]
 * - Example response [ undefined, Error ]
 *
 *
 * When used with Promise.all([req1, req2, req3])
 * - Example response [ [data1, data2, data3], undefined ]
 * - Example response [ undefined, Error ]
 *
 *
 * When used with Promise.race([req1, req2, req3])
 * - Example response [ data, undefined ]
 * - Example response [ undefined, Error ]
 *
 * @param {Promise} promise
 * @returns {Promise} [ data, undefined ]
 * @returns {Promise} [ undefined, Error ]
 */
const handle = (promise) => {
  return promise
    .then(data => ([data, undefined]))
    .catch(error => Promise.resolve([undefined, error]));
}

async function userProfile() {
  let [user, userErr] = await handle(getUser());

  if(userErr) throw new Error('Could not fetch user details');

  let [friendsOfUser, friendErr] = await handle(
    getFriendsOfUser(userId)
  );

  if(friendErr) throw new Error('Could not fetch user\'s friends');

  let [posts, postErr] = await handle(getUsersPosts(userId));

  if(postErr) throw new Error('Could not fetch user\'s posts');

  showUserProfilePage(user, friendsOfUser, posts);
}
Enter fullscreen mode Exit fullscreen mode

Using the handle utility function, we are able to avoid Unhandled promise rejection error and also handle error granularly.

Explanation

The handle utility function takes a promise as an argument and always resolves it, returning an array with [data|undefined, Error|undefined].

  • If the promise passed to the handle function resolves it returns [data, undefined];
  • If it was rejected, the handle function still resolves it and returns [undefined, Error]

Similar solutions

Conclusion

Async/await has a clean syntax, but you still have to handle thrown exceptions in async functions.

Handling error with .catch in promise .then chain can be difficult unless you implement custom error classes.

Using the handle utility function, we are able to avoid Unhandled promise rejection error and also handle error granularly.

Top comments (37)

Collapse
 
ironsavior profile image
Erik Elmore

Why bother capturing a thrown exception only to pass it up the call stack by return value so the caller has to explicitly check for it when you could use much simpler conventional exceptions mechanics?

function else_throw( fn, msg ){
  return async (...args) => fn(...args).catch(e => Error(`${msg} caused by: ${e}`));
}

const _gu = getUser;
getUser = else_throw(_gu, 'Could not fetch user details');

const _gfou = getFriendsOfUser;
getFriendsOfUser = else_throw(_gfou, "Could not fetch user's friends");

const _gup = getUsersPosts;
getUsersPosts = else_throw(_gup, "Could not fetch user's posts");

async function userProfile() {
  const user = await getUser();

  return showUserProfilePage(
    user,
    await getFriendsOfUser(user),
    await getUsersPosts(user)
  );
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
sobiodarlington profile image
Sobio Darlington • Edited

You are returning a promise in your else_throw function, which means you still need to catch/handle promise rejections with a .catch method or try...catch block inside of userProfile function

The use-case below will throw Unhandled promise rejection in any JS environment

function else_throw( fn, msg ){
  return async (...args) => fn(...args).catch(e => Error(`${msg} caused by: ${e}`));
}

function getUser() {
  return new Promise((resolve, reject) => {
    reject(new Error('An error occured'));
  });
}

async function test() {
  let _gu = getUser;
  let _getUser = else_throw(_gu, 'Could not fetch user details');

  console.log(await getUser());
}

test();
Enter fullscreen mode Exit fullscreen mode

Thanks for your feedback.

Collapse
 
ironsavior profile image
Erik Elmore • Edited

You have a bug on line:

console.log(await getUser());

Change to:

console.log(await _getUser());

The original example also allows errors to propagate from userProfile, but that's not the point of your post. Your post is about adding context to async errors. You would want to handle it at the call site to userProfile. You'd handle it in test(). The same is true of the code from the post.

Thread Thread
 
sobiodarlington profile image
Sobio Darlington • Edited

Thank you for pointing the bug out.

The original example also allows errors to propagate from userProfile, but that's not the point of your post.

It's just an example, you can decide to return a http response assuming it's an API server. Also propagating error is not bad else an error from a node library will return an error as a return value instead of throwing, which could cause a lot of issues in your program.

Your post is about adding context to async errors. You would want to handle it at the call site to userProfile.

From your example

async function userProfile() {
  const user = await getUser();

  return showUserProfilePage(
    user,
    await getFriendsOfUser(user),
    await getUsersPosts(user)
  );
}

On this line const user = await getUser();

Assuming user fails with a rejection, user variable will be assigned an Error object. Without the user data whats the point of calling getFriendsOfUser(user) and getUsersPosts(user) with an Error object?

In scenarios like this throwing or returning a response will be a better way to deal with the resulting error from getUser request than to pass it to another function to deal with it.

Lets say you pass user which contains an Error object to getFriendsOfUser(user) it'll result to an error inside of getFriendsOfUser function since getFriendsOfUser is expecting a user object and not an Error object, so instead of your program to throw an error about user details not found, it'll probably cause a totally different error inside of getFriendsOfUser. Imagine scenarios like this all over your codebase, it'll be difficult to debug.

If you intend to handle errors when userProfile is called, then you'll need to check results from your await calls if its an instance of Error, then you'll also check if return value of userProfile() is an instance of Error then do whatever with the Error object.

I'm in favour of handling errors where they occur and/or propagating to the top.

Thread Thread
 
ironsavior profile image
Erik Elmore • Edited

Assuming user fails with a
rejection, user variable will
be assigned an Error object.
Without the user data whats the
point of calling
getFriendsOfUser(user) and
getUsersPosts(user) with an
Error object?

user will not be assigned to an instance of Error and getFriendsOfUser() will not be called in the case where getUser() rejects because await will cause the program to throw out of userProfile(). The error will propagate upward from the awaiting function and will have a message indicating the getUser() call failed.

I would caution you against handling all errors where they occur. You're not necessarily wrong to do that, but be careful not to use that as a hard and fast rule everywhere. Errors should be handled only where the program should take a specific action. This will vary on a case by case basis. In this case, the specific action is to swap one error for another, (but the error is not otherwise handled).

Before I make my next point, I want to say that I like what you're trying to do and I appreciate that you are giving serious thought to patterns that implement exception policy. I wish more devs did that. Don't stop thinking about ways to abstract away and standardize exception handling behavior because every program has that problem.

My code and yours more or less do the same two things: replaces a low level error with another containing context about userProfile()'s intentions; and causes early exit of userProfile(). What's true of one program's error handling behavior is true of the other in terms of outcome.

The caller does not have to check the return value of userProfile() because it's using conventional error propagation paths. This means the caller could use try/catch within an async function that awaits the call to userProfile() or by using .catch() on the promise returned by userProfile(). The caller's option to not handle the error and allow the error to continue propagating normally is also preserved.

The main problem with your proposal is that it defeats a major benefit of using async function, which is being able to write async code that is not cluttered by explicit error handling.

This benefit can be seen in my version because the body of userProfile() contains no explicit error handling, contains no branching (objectively less complex code), and the errors propagated say what userProfile() was doing at the time of the error.

My version could arguably be improved by creating the decorated versions of getUser() (and the others) in the body of userProfile() instead of at program initialization and adding some dynamic context to the error messages (like user name or whatever is useful to capture in the log).

Thread Thread
 
sobiodarlington profile image
Sobio Darlington

Your approach is also good.

I understand your intention from this response.

When I tested your else_throw helper with a promise rejection, it returned and error object instead of throwing, which was the reason for my response. Unless I didn't test properly.

If it throws as expected then your approach will be a great alternative.

Thread Thread
 
ironsavior profile image
Erik Elmore

I must have forgotten to have it throw from the .catch() callback. I intended for it to throw.

Thread Thread
 
sobiodarlington profile image
Sobio Darlington

Yeah. That should be it.

Collapse
 
infinity1975 profile image
Robert Larsson • Edited

Why not only do this?

async function userProfile() {
  const user = await getUser().catch(() => throw Error('Could not fetch user details');

  const friendsOfUser = await getFriendsOfUser(user.userId).catch(() => throw Error('Could not fetch user\'s friends');

  const posts = await getUsersPosts(user.userId).catch(() => throw Error('Could not fetch user\'s posts'));

  showUserProfilePage(user, friendsOfUser, posts);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
abernier profile image
abernier

using throw inside .catch is the way to go to stop execution of the function. It's like our old if (err) return; ❤️

Collapse
 
sobiodarlington profile image
Sobio Darlington • Edited

Why not only do this?

I'm not clear with this, but you could throw inside of .catch to propagate it if you like.

Your example has syntax error though.

Collapse
 
infinity1975 profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
Robert Larsson • Edited

Complaining about syntax error is just really showing how little you want to take in what people say. The solution is what you do, but less complicated. And your idead is stolen from another guy who also commented here. And by the way, your solution don't have a userId so :)

Thread Thread
 
sobiodarlington profile image
Sobio Darlington • Edited

Sorry if my response came off that way. We have lots of beginners that might copy your example and not know why it's not working, that was why I pointed out the syntax error(It was unnecessary).

I didn't steal my post from that guy.

I got my handle function solution from a youtuber, which I linked to his article at the end of my post. I also linked an npm package "await-to" that did similar implementation.
If you want to accuse me of stealing, you should at least be correct with the origin of my solution.

Original source of my solution

Also I've been writing JavaScript for 8years, I don't need to steal a post to write about Promises/asynchronous JavaScript.

Collapse
 
cafesanu profile image
Carlos Sanchez

Pretty neat solution!. The only thing I changed was that the data is returned in the second position and the error in the first position. This way the developer is forced to at least think about handling the error.

...
  .then((data) => [undefined, data])
  .catch((error) => Promise.resolve([error, undefined]));
...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mbursill profile image
mbursill

Great solution! I've adapted it for TypeScript and added a defaultError so the error isn't falsy when not provided.

export const handle = <T>(
  promise: Promise<T>,
  defaultError: any = 'rejected'
): Promise<T[] | [T, any]> => {
  return promise
    .then((data) => [data, undefined])
    .catch((error) => Promise.resolve([undefined, error || defaultError]));
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
daveappz profile image
David

thanks!! you rock

Collapse
 
simonrobertson profile image
Simon Robertson

Sorry to drop a comment on an article from last year, but I have just been looking at ways to handle awaited Promise rejections. With source code not having to look like output code (with the help of Babel and/or Webpack) one way to handle Promise rejections would look something like this ...

let response = await fetchSomething() catch (error) { ... }

A plugin with AST access could convert that to something along the lines of ...

let response; try { response = await fetchSomething() } catch (error) { ... }

I agree that something needs to be done though otherwise we will end up in a situation where JavaScript source code is saturated with try/catch blocks. More and more APIs are returning Promises these days, even Node has started to do it.

Collapse
 
sobiodarlington profile image
Sobio Darlington • Edited

This is a great approach.

I don't do this checks in my controllers when writing production code.

I mostly use my handle function when I have no control over what is returned from an asynchronous request/operations.

I'll experiment with your approach and probably write a post on it.

Collapse
 
hannesvz profile image
Hannes van Zyl

I have been trying to come around to using promises "correctly" and moving away from using callbacks, where catching errors was usually a bit more elegant in my opinion.

I'm still sort of yearning for callbacks and using async but I suppose that is in the past now.

Your handle function really nails it for me, it was just what I was looking for. Great post and explanation :)

Collapse
 
4n0nym0u5n1nj4 profile image
4n0nym0u5n1nj4 • Edited

why you put Promise.resolve in catch function?

const handle = (promise) => {
  return promise
    .then(data => ([data, undefined]))
    .catch(error => ([undefined, error]));
}

is ok.

Collapse
 
samartuso profile image
Sam Artuso

I agree, there's no reason for putting Promise.resolve in the catch().

Collapse
 
calvintwr profile image
calvintwr

You are much much better off just using #catch at the top level caller.


const fetchUsers = async () => {
    return await fetch('')
}

const fetchPosts = async () => {
    throw Error('error in getting posts')
    return await fetch('')
}

const fetchEverything() = async() => {
    return [
        fetchUsers(),
        fetchPosts()
    ]
}

// fetchEverything, as the top level caller, should have the catch handler
fetchEverything().then(results => {
    // do something with results
}).catch(err => {
    // do something with err. You have two non-mutually-exclusive options.

    // console.log and don't crash anything
    console.log(err)
    alert('Something has failed. Please try again later.')

    // AND/OR, you can throw the error to "bubble up" the error.
    // If there is another caller with a catch handler, this error will bubble up there.
    // Otherwise, it will get thrown into the main scope to stop execution.
    throw err
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
davidmark profile image
David Mark • Edited

What an odd coincidence, as I too posted an article about Promise error handling, with an example that uses a hypothetical user profile interface. It was about five days before this one was posted. May I assume it served as some sort of inspiration? Granted, your aim here is very different, so I'm surely not accusing you of copying me. Just curious.

linkedin.com/pulse/promise-promise...

Have you read the follow-up, which was posted a few days later?

linkedin.com/pulse/waiting-good-co...

Note that uncaught rejections do not throw in Chrome, at least not at this time with the version I have. They log an error to the console, which is not nearly as useful for debugging.

Collapse
 
sobiodarlington profile image
Sobio Darlington

I haven't seen your post as I don't use LinkedIn(I'm not proud of it). I'll check out your post though.

Collapse
 
davidmark profile image
David Mark

I think you will find it useful. I guess we just had similar ideas for the hypothetical functions in similar subject matter.

Google suggested I check out your article the day after I posted my follow-up. Enjoy!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.