Use promises and await with Node.js callbacks

By

Learn how to use promises and async/await with Node.js callback-based functions by wrapping them with promisify from the util module to escape callback hell.

~~~

A lot of older Node.js APIs were built before promises were common, so they expose a callback-based API.

The typical pattern looks like this:

doSomething(param, (err, result) => {

})

Many core modules now ship a promise-based version, like node:fs/promises. Prefer those when they exist.

Libraries changed too. This post was born from node-redis: while working with it on a project I had too many levels of callbacks nested into each other, a perfect “callback hell”. Since version 4 the official client returns promises, so today you await it directly:

import { createClient } from 'redis'

const client = createClient()
await client.connect()

const currentUserName = await client.hGet(`user:${req.session.userid}`, 'username')
const followers = await client.sMembers(`followers:${currentUserName}`)
const users = await client.hKeys('users')

For connecting, TTLs, the data types, pub/sub and closing the client, read How to use Redis from Node.js.

promisify is still useful when you hit a true callback API: an old dependency, Passport’s req.login, or custom code that never moved to promises.

Also, sometimes it’s necessary to avoid callbacks because you need to return from the function the result of a function call. If that’s returned in a callback, the only way to get the result back would be to send it back with a function, and the callback party continues:

const myFunction = () => {
  doSomething(param, (err, result) => {
    return result //can't return this from `myFunction`
  })
}
const myFunction = callback => {
  doSomething(param, (err, result) => {
    callback(result) //no
  })
}

myFunction(result => {
  console.log(result)
})

There’s an easy solution for those cases, provided by Node.js itself.

We can “promisify” any function that does not support promises (and as a consequence the async/await syntax) by importing promisify from the core Node.js util module:

import { promisify } from 'node:util'

Then we create new functions using it:

const ahget = promisify(client.hget).bind(client)
const asmembers = promisify(client.smembers).bind(client)
const ahkeys = promisify(client.hkeys).bind(client)

See how I added the a letter to mean async. This was the old callback-based redis client. With the current one you skip this step and call the promise methods directly.

Now we can change this example “callback hell”:

client.hget(`user:${req.session.userid}`, 'username', (err, currentUserName) => {
  client.smembers(`followers:${currentUserName}`, (err, followers) => {
    client.hkeys('users', (err, users) => {
      res.render('dashboard', {
        users: users.filter((user) => user !== currentUserName && followers.indexOf(user) === -1)
      })
    })
  })
})

into a much cleaner:

const currentUserName = await ahget(`user:${req.session.userid}`, 'username')
const followers = await asmembers(`followers:${currentUserName}`)    
const users = await ahkeys('users')

res.render('dashboard', {
  users: users.filter((user) => user !== currentUserName && followers.indexOf(user) === -1)
})

This is optimal when using a function you don’t have access to, like a 3rd party library that only exposes callbacks. If the library already returns promises, just await it.

Under the hood, promisify wraps the function in a promise, and returns it.

You can do this manually, too, returning a promise from a function, and then using it with async/await:

const handleLogin = (req, user) => {
  return new Promise((resolve, reject) => {
    req.login(user, (err) => {
      if (err) {
        return reject({
          error: true,
          message: err,
        })
      }
      return resolve({
        success: true,
      })
    })
  })
}

//...
const resultLogin = await handleLogin(req, user)
Tagged: Node.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about node: