DEV Community

Cover image for Using Async/await in Express
Zell Liew πŸ€—
Zell Liew πŸ€—

Posted on • Originally published at zellwk.com

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.

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 known as called "controllers". I prefer calling them request handlers because request handlers are more explicit).

app.post("/testing", async (req, res) => {
  // Do something here
});
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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'
  })
}
Enter fullscreen mode Exit fullscreen mode

This request results in an error. Unfortunately, Express will not be able to handle this error. You'll receive a log like this:

Unhandled Promise Rejection Warning.

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

Logging the error into the console.

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Set the HTTP status to 500
  2. Send a Text response back to the requester
  3. Log the text response in the console

Sends a text response back to the request
I used Postman to send a request to my server. Here's the text response back from the server.

Logs the text response into the console.
Notice the 500 HTTP Status log in this image. This tells me Express's default handler changed the HTTP Status to 500. The log is from Morgan. E 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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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) => {
  function runAsync () {
    await firstThing()
    await secondThing()
  }

  runAsync()
    .catch(next)
})
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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).

To use express-async-handler, you have to install it first:

npm install express-async-handler --save
Enter fullscreen mode Exit fullscreen mode

Using it in your app:

const asyncHandler = require('express-async-handler')

app.post('/signup', asyncHandler(async(req, res) => {
    await firstThing()
    await secondThing()
})
Enter fullscreen mode Exit fullscreen mode

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.


Thanks for reading. This article was originally posted on my blog. Sign up for my newsletter if you want more articles to help you become a better frontend developer.

Top comments (3)

Collapse
 
rokuem profile image
Mateus Amorim

some things to note here:

  • I remember seeing in a article that try / catch blocks are not minified, haven't tested it out yet, but it's something to be careful about.
  • While you can use try / catch blocks, note that:
    1. The async function returns a Promise
    2. You usually would have the request handler declared in a controller file and have services functions handle the operations.
    3. Promises can handle errors.

So you could also do things like this:



// user/services/user.service.getAll.ts
export default async function getAllUsers(filter: string, page: number): Promixe<IUser[]> {
   //...
}

// user/controllers/user.get.controller.ts
/*
* controller documentation...
*/
async function userGetController(req, res, next): Promise<void> {
   let { filter, page } = req.query;
   filter = filter || '';
   page = parseInt(page) || 1;
   const users = await getAllUsers(filter, page)
   .then(users => res.json(users))
   .catch(function getAllUsersControllerError(e) {
     let status = 500;
     let errorObject: IApiError = {
       //...
     }

     const returnedApiError = !!e.status;

     if (returnedApiError) {
       status = e.status;
       errorObject = e;
     }

     res.status(status).json(errorObject);
   });
}

// user/user.router.ts
app.get('/users', userGetController);

// or if you want to be even more safe

app.get('/users', function userGetRoute(req, res, next): Promise<void> {
  return userGetController(req, res, next).catch(e => {
     // do something...
  });
});
```

Enter fullscreen mode Exit fullscreen mode
Collapse
 
ogrotten profile image
ogrotten

Thank you SO MUCH for not using the wholly NOT real world settimeout() in your examples. Here's something that I can read thru and see actually applied.

Collapse
 
mikaelhadler profile image
Mikael Hadler

Amazing, thank you!