You can have proper Promise and async/await support in express, today.

Here’s what it looks like:

'use strict';

let express = require('express');
let app = require('@root/async-router').Router();

// simulate async / promises
app.get('/hello', async function (req, res, next) {
    return await Promise.resolve({ message: 'Hello, World!' });
});

// simulate unhandled error
app.get('/error', async function (req, res, next) {
    await Promise.reject(new Error("It's a mad world..."));
});

// remember: error handlers must come last
app.use('/', function errorHandler(err, req, res, next) {
    console.error('Unhandled Error:');
    console.error(err);
    res.statusCode = 500;
    res.end('Internal Server Error');
});

// assign default (async) router
// note: error handlers MUST come last
let httpHandler = express().use('trust proxy', false).use('/', app);

if (require.main !== module) {
    module.exports = httpHandler;
    return;
}

require('http')
    .createServer(httpHandler)
    .listen(3000, function () {
        console.info('Listening on', this.address(), '...');
    });

All this you could test with curl:

# 200 OK Hello, World!
curl http://localhost:3000/hello

# 500 Internal Server Error
curl http://localhost:3000/error

As you can see, @root/asyncrouter (forked from express-promisify-router) is a drop-in solution that you can start using today to begin simplifying your express routes, and continuing to work the way in the way that

  • drop-in replacement - NO refactoring required
  • intuitive - works just like you’d expect
  • lightweight - a single dependency in < 100 lines of code

It’s really just a rather plain wrapper around express.Router(). Nothing to be worried about in terms of security and no breaking changes.

Installation

Install Node.js, if you haven’t already:

curl -sSL https://webinstall.dev/node@lts | bash

And install the necessary modules from npm:

npm init
npm install --save express @root/async-router

What’s Different?

The key difference comes down to changing how you create your default router:

// Don't do this anymore!
let app = express();
// Do this instead:
let app = require('@root/async-router').Router();

Likewise, you’ll also use @root/async-router for any “mini apps” or sub routers:

let adminApp = require('@root/async-router').Router();

// ...

app.use('/api/admin', adminApp);

If you do that you’ll be able to return Promises and use async functions. Optionally, you can simply return the result object rather than calling res.json().

These two are equivalent:

// async function awaiting a promise and explicitly calling res.json()
app.use('/hello', async function (req, res, next) {
    // Pretend that this does something asynchronous and useful :)
    var result = await Promise.resolve({ message: 'Hello, World!' });
    res.json(result);
});
// returning a promise, implicitly calling res.json() on the result
app.use('/hello', function (req, res, next) {
    return new Promise(function (resolve, reject) {
        resolve({ message: 'Hello, World!' });
    });
});

However, you’ll still need to use the top-level express for any global options that you might need to set - such as trust proxy - as well as the default router (the main “app”) and any error handlers:

let httpHandler = express()
    .set('trust proxy', true)
    .use('/', app)
    .use('/', errorHandler);
// Side note: I don't recommend this, see notes below.
//.listen(3000)

module.exports = httpHandler;

The error handler will now catch rejected promsises and async thrown errors.

You can call .listen() on the express server, however, I prefer to keep things simple and use call .listen() the normal way:

if (require.main === module) {
    http.createServer(httpHandler).listen(3000, function () {
        console.info('Started express on', this.address());
    });
}

The main benefits are that 1) it’s less confusing to the reader what’s going on and 2) you’re not losing the reference to the http server, which you’ll need for web sockets and other http-level features.

The Missed Opportunity of Express 5.x

Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error.

Over half a decade ago Express 5.x was promised - and great progress was made.

In fact, if you’re willing to run npm install express@next you can get express@5.0.0-alpha.8, which actually has working promise support “already”.

However, 5.x never shipped, and express is a dead project. Or perhaps if you prefer, a complete project. Either way, Express 5.x will almost certainly never be released.

The promised express-native support for Promises and async/await support simply isn’t coming.

express-async, etc al: Confusing Nonsense

There are a number of decently popular solutions to using async routes in express but, I can’t fathom why.

Most of what I found, including express-async-router, were much more complex and require breaking changes to your app - the implementations just don’t make sense.

koa: Makes sense

If you’re okay with breaking APIs and rewriting your app - or you’re starting a new project from scratch - koa could be a great solution.

To me this is a much better solution than creating routers that work against express’ design.

express-promisify-router: A Diamond in the Rough

I forked @root/async-router from express-promisify-router in order to bugfix the error handling.

Despite its lack of stars, it was clearly the best option of all that existed for working with promises and async functions in express - not just the best, but the ideal (well, aside from the error handling issue).