codeburst

Bursts of code to power through your day. Web Development articles, tutorials, and news.

Follow publication

Node/Express: async code and error handling

Assume you want to write some backend using node/express. That’s the good idea, it’s easy — you just write

const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello'))app.listen(3000)

and that’s all you need to start. Then you realize that any backend is usually a queue of asynchronous operations, and there’re different ways to organize asynchronous code in node — you can use callbacks, promises or async/await. That’s not simple to choose. In this article I’ll describe my way, I’ll show how to write relatively short code with good error handling using both promises and async/await approach.

The Task

Assume we have to complete user login action that consists on several steps like:

  • check that login is specified in the request;
  • find the user by login;
  • check that password is valid using bcrypt;
  • update user last_login_at time (non critical);
  • generate user auth token and return it.

That’s the queue of asynchronous operations, any operation could fail, and we want to have exact information about any error. One operation (update last_login_at) is not critical, login should work even if it fails.

Callback Hell

Let’s start with callbacks. Suppose we’ve written separate functions for every step like findUser(login, callback), checkPassword(plainTextPassword, user, callback), etc. By the convention the first callback argument refers to error, and the second is the result of operation (if there’s no error). The route code could look like this:

routes.post('/login', (req, res) => {
checkFields(req, error1 => {
if (error1) return errorResult(res, 400, error1)
findUser(req.body.login, (error2, user) => {
if (error2) return errorResult(res, 400, error2)
checkPassword(req.body.password, user, (error3, result) => {
if (error3) return errorResult(res, 500, error3)
if (!result) return errorResult(res, 403, 'Password is invalid')
updateUserLastLoginAt(user, error4 => {
if (error4) logger.add(error4)
generateToken(user, (error4, token) => {
if (error4) return errorResult(res, 500, error4)
return successResult(res, token)
})
})
})
})
})
})

Every step adds new nested level, and the code becomes hard to read and maintain very fast, this called the callback hell. But there’s an advantage with this approach, notice that we have exact errors on every step, and we are free to do anything with it, for example the code above adds custom server response code for every error. It’s easy to break the chain in case of critical error by simply returning the value. It’s simple to perform operations one after another, but it’s hard to make something in parallel (although that’s not a big problem because parallel operations are rare in usual web server backend). All popular libraries support callback-approach.

Promises

Now assume that our function findUser or checkPassword returns a promise. We’ll rewrite action using promises but first will check if we are on the same wave. Consider this code:

Promise
.resolve()
.then(() => Promise.reject('error1'))
.catch(console.log)
.then(() => console.log('continue1'))

Promise
.resolve()
.then(() => Promise.reject('error2'))
.then(() => console.log('continue2'), console.log)
.then(() => console.log('continue3'))

This will ouput error1 -> error2 -> continue1 -> continue3. It will output continue1 because the previous catch resolves error1, and it will skip continue2 because instead of this the second argument error handler will run. That’s essential to understand that if the catch function does not return anything, the undefined result counts as resolve, and the chain will continue to run.

If the error1 is critical, and we have to break the rest of promise chain, then the catch handler should throw an error by itself (or return a rejected promise).

routes.post('/login', (req, res) => {
let user = null
Promise
.resolve()
.then(() => checkFields(req))
.then(() => findUser(req.body.login))
.then(result => {
user = result
})
.then(() => checkPassword(req.body.password, user))
.then(() => {
// Non critical action has it's own error handler
// Nested promise chain is here:
return updateUserLastLoginAt(user).catch(logger.add)
})
.then(generateToken(user))
.then(token => successResult(res, token))
.catch(error => errorResult(res, error))
})

It looks nicer, but we loose error control here — there’s one critical error handler for the whole chain. We can not handle error separately (for example, send custom server response code), especially if the error was triggered by some external library we do not control. To get the control back we have to add sub chain for every step like this:

.then(() => {
return findUser(req.body.login).catch(error => {
doAnythingWithError(error)
throw error //<-- THIS IS ESSENTIAL FOR BREAKING THE CHAIN
return Promise.reject(error) //<-- EITHER THIS
})
})

That’s the difference between critical and non-critical errors. If we want to break the root chain, we have to add throw error or Promise.reject(error) into the sub chain error handler.

Async/Await

const user = await findUser(req.body.login)

Where does the error handling live here? If that’s our function, we can append the output with an error and use it like this:

const {error, user} = await findUser(req.body.login)

But the common practice is to return the plain result and throw an error:

try {
const user = await findUser(req.body.login)
} catch (error) {
...
}

Async/await is not a magic — there’s no easy error handling of individual errors that happen inside try/catch block. We have to wrap every command into it’s own try/catch block, handle the error and rethrow it to the external try/catch if we have to stop the further steps:

routes.post('/login', async (req, res) => {
try {
...
let user = null
try {
user = await findUser(req.body.login)
} catch (error) {
doAnythingWithError(error)
throw error //<-- THIS IS ESSENTIAL FOR BREAKING THE CHAIN
}
...
} catch (error) {
errorResult(res, error)
}

Combining async/await and promises

We can use async/await and promises together. Let’s handle individual errors inside a promise and wrap everything with try/catch:

try {
...
const user = await findUser(req.body.login).catch(error => {
doAnythingWithError(error)
throw error
})
...
} catch (error) {
errorResult(res, error)
}

I like this approach the most. Let’s add some helper functions (taken from real project):

throwError = (code, errorType, errorMessage) => error => {
if (!error) error = new Error(errorMessage || 'Default Error')
error.code = code
error.errorType = errorType
throw error
}
throwIf = (fn, code, errorType, errorMessage) => result => {
if (fn(result)) {
return throwError(code, errorType, errorMessage)()
}
return result
}
sendSuccess = (res, message) => data => {
res.status(200).json({type: 'success', message, data})
}
sendError = (res, status, message) => error => {
res.status(status || error.status).json({
type: 'error',
message: message || error.message,
error
})
}

Now we can handle both Not Found and Error cases in one command:

const user = await User
.findOne({where: {login: req.body.login}})
.then(
throwIf(r => !r, 400, 'not found', 'User Not Found'),
throwError(500, 'sequelize error')
)
//<-- After that we can use `user` variable, it's not empty

Here we use then(success, error) construction, we catch database error and the same time throw an error in case if the result is empty. The error contains it’s original data, extended with errorType (db error, not found, etc) and corresponding server response code. The full login action code looks like this:

routes.post('/login', async (req, res) => {
try {
if (!req.body.login) throwError(400, 'incorrect request')()
const user = await User
.findOne({where: {login: req.body.login}})
.then(
throwIf(r => !r, 400, 'not found', 'User not found'),
throwError(500, 'sequelize error')
)
await bcrypt
.compare(req.body.password, user.password)
.then(
throwIf(r => !r, 400, 'incorrect', 'Password is incorrect'),
throwError(500, 'bcrypt error')
)
await user
.update({lastlogin_at: sequelize.fn('NOW')})
.catch(error => {
logger.log('sequelize update error', error)
// Do not throw an error because this action is optional
})
const token = await jsonwebtoken.sign(user.id, '***') sendSuccess(res, 'User logged in')({token})
} catch(error) {
sendError(res)(error)
}
})

Using the same helper functions we can complete any express action. Here’s one more, simpler example for the route that just retrieves some data by id using sequelize Project model:

routes.get('/projects/:id(\\d+)', async (req, res) => {
try {
if (!req.params.id) throwError(400, 'request error', 'Project `id` request parameter is invalid')
await Project
.findById(req.params.id)
.then(
throwIf(r => !r, 404, 'not found', 'Project not found'),
throwError(500, 'sequelize error')
)
.then(sendSuccess(res, 'Project Data Extracted'))
} catch (error) {
sendError(res)(error)
})

✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (5)

Write a response