Kasheftin
Kasheftin

Reputation: 9493

async / await proper error handling

Assume we have an action that runs on user login (express, node). This is the code that works, written using a lot of callbacks:

checkIfEmailAndPasswordAreSet(email, password, (error, response) => {
  if (error) return errorResponse(403, 'validation error', error)
  findUserByEmail(email, (error, user) => {
    if (error) return errorResponse(500, 'db error', error)
    if (!user) return errorResponse(403, 'user not found')
    checkUserPassword(user, password, (error, result) => {
      if (error) return errorResponse(500, 'bcrypt error', error)
      if (!result) return errorResponse(403, 'incorrect password')
      updateUserLastLoggedIn(user, (error, response) => {
        if (error) return errorResponse(500, 'db error', error)      
        generateSessionToken(user, (error, token) => {
          if (error) return errorResponse(500, 'jwt error', error)
          return successResponse(user, token)
        })
      })
    })
  })
})

I want to rewrite this code using async/await and avoid callback hell. How to do that?

The first attempt could look like that:

try {
  await checkIfEmailAndPasswordAreSet(email, password)
  const user = await findUserByEmail(email)
  if (!user) throw new Error('user not found')
  const result = await checkUserPassword(user, password)
  if (!result) throw new Error('incorrect password')
  await updateUserLastLoggedIn(user)
  const token = await generateSessionToken(user)
  return successResponse(user, token)
} catch (e) {
  // How to handle the error here?
}

I want to keep the proper error handling, that is if the error was thrown in checkUserPassword method, I want the response to contain info about this. What should I write in the catch method?

For example, I could wrap every instruction into it's own try / catch block like that:

try {

  let user, result

  try {
    await checkIfEmailAndPasswordAreSet(email, password)
  } catch (error) {
    throw new Error('This error was thrown in checkIfEmailAndPasswordAreSet')
  }

  try {
    user = await findUserByEmail(email)
  } catch (error) {
    throw new Error('This error was thrown in findUserByEmail')
  }

  if (!user) throw new Error('user not found')

  ...
} catch (error) {
  return errorResponse(error)
}

But this code.. probably that's not a callback hell, but I would call it try/catch hell. It takes at least 2 times more rows that the original old fashioned code with callbacks. How to rewrite it to be shorter and take an advantage of async/await?

Upvotes: 10

Views: 4125

Answers (1)

Bergi
Bergi

Reputation: 664930

There is no easy handling of individual errors with try/catch.

I would probably write some helper functions to facilitate simple error throwing:

function error(code, msg) {
    return e => {
        e.status = code;
        e.details = msg;
        throw e;
    };
}
function ifEmpty(fn) {
    return o => o || fn(new Error("empty"));
}

and then use standard promise methods:

try {
    await checkIfEmailAndPasswordAreSet(email, password)
        .catch(error(403, 'validation error'));
    const user = await findUserByEmail(email)
        .then(ifEmpty(error(403, 'user not found')), error(500, 'db error', error));
    await checkUserPassword(user, password)
        .then(ifEmpty(error(403, 'incorrect password')), error(500, 'bcrypt error'));
    await updateUserLastLoggedIn(user)
        .catch(error(500, 'db error'));
    const token = generateSessionToken(user)
        .catch(error(500, 'jwt error'));
    return successResponse(user, token);
} catch(err) {
    errorResponse(err.status, err.details, err);
}

Upvotes: 14

Related Questions