Use promises and await with Node.js callbacks
By Flavio Copes
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)Want me to talk about your product? You can sponsor this site.
Related posts about node: