Using async/await in Express
Have you noticed you write a lot of asynchronous code in Express request handlers? This is normal because you need to communicate with the database, the file system, and other APIs.
When you have so much asynchronous code, it helps to use async/await. It makes your code easier to understand.
Today, I want to share how to use async/await in an Express request handler.
Note: Before you continue, you need to know what async/await is. If you don’t know, you can read this article for more information.
Update: Express 5.0 baked the handling of async errors into the framework itself — the error will be sent to the error handling middleware even if the Promise rejects. This means the rest of this article is obselete once Express 5 comes out of beta.
I’ll update this article again Express 5 is out of beta.
Using async/await with a request handler
To use async/await, you need to use the async
keyword when you define a request handler. (Note: These request handlers are also called “controllers”. I prefer calling them request handlers because “request handlers” is more explicit).
app.post('/testing', async (req, res) => {
// Do something here
})
Once you have the async
keyword, you can await
something immediately in your code.
app.post('/testing', async (req, res) => {
const user = await User.findOne({ email: req.body.email })
})
Handling Async errors
Let’s say you want to create a user through a POST request. To create a user, you need to pass in a firstName
and an email
address. Your Mongoose Schema looks like this:
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
firstName: {
type: String,
required: true
}
})
Here’s your request handler:
app.post('/signup', async (req, res) => {
const { email, firstName } = req.body
const user = new User({ email, firstName })
const ret = await user.save()
res.json(ret)
})
Now, let’s say you send a request that lacks an email address to your server.
fetch('/signup', {
method: 'post'
headers: { 'Content-Type': 'application/json' }
body: JSON.stringify({
firstName: 'Zell'
})
})
This request results in an error. Unfortunately, Express will not be able to handle this error. You’ll receive a log like this:
To handle an error in an asynchronous function, you need to catch the error first. You can do this with try/catch
.
app.post('/signup', async (req, res) => {
try {
const { email, firstName } = req.body
const user = new User({ email, firstName })
const ret = await user.save()
res.json(ret)
} catch (error) {
console.log(error)
}
})
Next, you pass the error into an Express error handler with the next
argument.
app.post('/signup', async (req, res, next) => {
try {
const { email, firstName } = req.body
const user = new User({ email, firstName })
const ret = await user.save()
res.json(ret)
} catch (error) {
// Passes errors into the error handler
return next(error)
}
})
If you did not write a custom error handler yet, Express will handle the error for you with its default error handler. (Though I recommend you write a custom error handler. You can learn more about it here).
Express’s default error handler will:
- Set the HTTP status to 500
- Send a Text response back to the requester
- Log the text response in the console
I talked about Morgan in detail here.
Handling two or more async errors
If you need to handle two await
statements, you might write this code:
app.post('/signup', async (req, res, next) => {
try {
await firstThing()
} catch (error) {
return next(error)
}
try {
await secondThing()
} catch (error) {
return next(error)
}
})
This is unnecessary. If firstThing
results in an error, the request will be sent to an error handler immediately. You would not trigger a call for secondThing
. If secondThing
results in an error, firstThing
would not have triggered an error.
This means only one error will be sent to the error handler. It also means we can wrap all await
statements in ONE try/catch
statement.
app.post('/signup', async (req, res, next) => {
try {
await firstThing()
await secondThing()
} catch (error) {
return next(error)
}
})
Cleaning up
It sucks to have a try/catch
statement in each request handler. They make the request handler seem more complicated than it has to be.
A simple way is to change the try/catch
into a promise. This feels more friendly.
app.post('/signup', async (req, res, next) => {
async function runAsync () {
await firstThing()
await secondThing()
}
runAsync().catch(next)
})
But it’s a chore to write runAsync
for every Express handler. We can abstract it into a wrapper function. And we can attach this wrapper function to each request handler
function runAsyncWrapper (callback) {
return function (req, res, next) {
callback(req, res, next)
.catch(next)
}
}
app.post('/signup', runAsyncWrapper(async(req, res) => {
await firstThing()
await secondThing()
})
)
Express Async Handler
You don’t have to write runAsyncWrapper
code each time you write an express app either. Alexei Bazhenov has created a package called express-async-handler that does the job in a slightly more robust way. (It ensures next
is always the last argument).
Update: I found another package that’s much easier to use compared to express-async-handler. More on this below.
To use express-async-handler
, you have to install it first:
npm install express-async-handler --save
Using it in your app:
const asyncHandler = require('express-async-handler')
app.post(
'/signup',
asyncHandler(async (req, res) => {
await firstThing()
await secondThing()
})
)
I don’t like to write asyncHandler
. It’s quite long. My obvious solution is to abbreviate asyncHandler
to ash
.
If you’re fancier, you can consider using @awaitjs/express by Valeri Karpov. It adds methods like getAsync
and postAsync
to Express so you don’t have to use express-async-handler
.
Express Async Errors
There’s a package called express-async-errors that makes Express errors much easier to handle. You just have to require it once and it’ll take care of the rest.
Installing it:
npm install express-async-errors -S
Using it:
const express = require('express')
require('express-async-errors')
That’s it!