How to convert between callbacks and Promises in Javascript
How to use the promisify and callbackify functions, and when they are not enough
While Promises and async/await are increasingly the primary way to write asynchronous code in Javascript, callbacks are still used in many places. Several libraries that adopted that style are slow to migrate to the more modern alternative, and browser (and Node) APIs are also slow to change.
For example, marked, a markdown compiler needs a callback when it's used in asynchronous mode:
marked(text, options, (err, result) => {
// result is the compiled markdown
});
Similarly, setTimeout
invokes a function when the time is up:
setTimeout(callback, 100);
Not to mention a ton of web APIs, such as Indexed DB, FileReader, and others. Callbacks are still everywhere, and it's a good practice to convert them to Promises especially if your code is already using async/await.
Callback styles
Callbacks implement the continuation-passing style programming where a function instead of returning a value calls a continuation, in this case, an argument function. It is especially prevalent in Javascript as it does not support synchronous waiting. Everything that involves some future events, such as network calls, an asynchronous API, or a simple timeout is only possible by using a callback mechanism.
There are several ways callbacks can work. For example, setTimeout
uses a callback-first pattern:
setTimeout(callback, ms);
Or functions can get multiple functions and call them when appropriate:
const checkAdmin = (id, isAdmin, notAdmin) => {
if (/* admin logic */) {
isAdmin();
}else {
notAdmin();
}
};
How the callback is invoked can also vary. For example, it might get multiple arguments:
const getUserData = (id, cb) => {
const user = /* get user */
cb(user.id, user.profile, user.avatar);
};
Also, asynchronicity might be implemented as an event emitted by an EventEmitter object:
const reader = new FileReader();
reader.onload = (event) => {
// event.target.result
}
reader.onerror = (error) => {
// handle error
};
reader.readAsDataURL(blob);
Node-style callbacks
As there are multiple equally reasonable ways to implement callbacks, it was a mess at first. A de-facto standard emerged, which is now called Node-style or error-first callbacks. It is used almost everywhere in Javascript whenever a callback is needed.
A Node-style callback has three characteristics:
- The callback function is the last argument
- It is called with an error object first, then a result (
(error, result)
) - It returns only one result
As an illustration, this function implements a Node-style callback:
const getUser = (id, cb) => {
const user = /* get user */;
if (user) {
// success
cb(null, user);
}else {
// error
cb(new Error("Failed to get user"));
}
};
Notice that when there is no error, the first argument is null
. This allows the caller to easily check whether the execution failed or succeeded:
getUser(15, (error, result) => {
if (error) {
// handle error
} else {
// handle result
}
});
With a callback structure that is used almost exclusively, it is possible to convert between Promises and callbacks more easily.
Convert callbacks to Promises
This is the more prominent direction as it's a common task to integrate a callback-based function into an async/await flow. Let's see how to do it!
Promise constructor
The Promise constructor is the low-level but universally applicable way to convert callbacks to Promises. It works for every callback style and it needs only a few lines of code.
The Promise constructor gets a function with two arguments: a resolve
and a reject
function. When one of them is called, the Promise will settle with
either a result passed to the resolve
function, or an error, passed to the reject
.
new Promise((res, rej) => {
// resolve with a value
// res(value)
// reject with error:
// rej(error)
})
This makes it easy to call a callback-based function and convert it to a Promise which then can be await
-ed:
// Node-style
new Promise((res, rej) => getUser(15, (err, result) => {
if (err) {
rej(err);
}else {
res(result);
}
}))
// setTimeout
new Promise((res) => setTimeout(res, 100));
// event-based
new Promise((res, rej) => {
const reader = new FileReader();
reader.onload = (event) => {
// resolve the Promise
res(event.target.result);
}
reader.onerror = (error) => {
// reject the Promise
rej(error)
};
reader.readAsDataURL(blob);
});
Promisified functions
The above examples show how to call a callback-based function and get back a Promise, but it requires wrapping every call with the Promise boilerplate. It would be better to have a function that mimics the original one but without the callback. Such a function would get the same arguments minus the callback and return the Promise.
To make promisified versions of functions, it is only a matter of wrapping the Promise constructor in a function that gets the arguments before the callback:
// setTimeout
const promisifiedSetTimeout = (ms) => new Promise((res) => setTimeout(res, ms));
// FileReader
const promisifiedFileReader = (blob) => new Promise((res, rej) => {
const reader = new FileReader();
reader.onload = (event) => {
res(event.target.result);
}
reader.onerror = (error) => {
rej(error)
};
reader.readAsDataURL(blob);
});
// checkAdmin
const promisifiedCheckAdmin = (id) => new Promise((res) => {
if (/* admin logic */) {
res(true);
}else {
res(false);
}
};
These functions are direct replacements to the callback-based originals and can be directly used in an async/await workflow:
// timeout
setTimeout(() => {
console.log("Timeout reached");
}, 100);
await promisifiedSetTimeout(100);
console.log("Timeout reached");
// checkAdmin
const checkAdmin = (id, isAdmin, notAdmin) => {
if (/* admin logic */) {
isAdmin();
}else {
notAdmin();
}
};
checkAdmin(15, () => {
console.log("User is an admin");
}, () => {
console.log("User is not an admin");
});
const admin = await promisifiedCheckAdmin(15);
console.log(`User is admin: ${admin}`);
// FileReader
const dataURI = await promisifiedFileReader(blob);
util.promisify
While the Promise constructor offers a universal way to transform callbacks to Promises, when the callback pattern follows the Node-style there is an easier
way. The util.promisify
gets the callback-based function and returns a promisified version.
import util from "util";
const promisifiedGetUser = util.promisify(getUser);
const user = await promisifiedGetUser(15);
// user is the result object
When the promisified function is called, the callback argument is added automatically and it also converts the result (or error) to the return value of the Promise.
How it works is no magic though. It uses the Promise constructor pattern we've discussed above, and uses the spread syntax to allow arbitrary amount of arguments. A simplified implementation looks like this:
const promisify = (fn) => (...args) => new Promise((res, rej) => {
fn(...args, (err, result) => {
if (err) {
rej(err);
}else {
res(result);
}
});
});
Usual problems
Handle this
The value of this
is complicated in Javascript. It can get different values depending on how you call a function.
For example, classes can have instance variables, attached to this
:
class C {
constructor() {
this.var = "var";
}
fn() {
console.log(this.var);
}
}
But when you have an object of this class, whether you call this function directly on the object or extract it to another variable makes a difference in the
value of this
:
new C().fn(); // var
let fn = new C().fn;
fn(); // TypeError: Cannot read property 'var' of undefined
This affects how to promisify the methods of this object as the util.promisify
requires just the function and not the whole object. Which, in turn,
breaks this
.
For example, let's say there is a Database object that creates a connection in its constructor then it offers methods to send queries:
class Database {
constructor() {
this.connection = "database connection";
}
getUser(id, cb) {
if (!this.connection) {
throw new Error("No connection");
}
setTimeout(() => {
if (id >= 0) {
cb(null, `user: ${id}`);
}else {
cb(new Error("id must be positive"));
}
}, 100);
}
}
const database = new Database();
database.getUser(15, (error, user) => {
// handle error or user
})
Using util.promisify
would break the getUser
function as it changes the value of this
:
const promisifiedDatabaseGetUser = util.promisify(database.getUser);
await promisifiedDatabaseGetUser(15); // Error: Cannot read property 'connection' of undefined
To solve this, you can bind the object to the function, forcing the value of this
:
const promisifiedDatabaseGetUser = util.promisify(database.getUser.bind(database));
const user = await promisifiedDatabaseGetUser(15);
function.length
The length
of a function is how many arguments it needs. Don't confuse this with the Array's length
, as that is how many elements in the array.
For example, this function needs 2 arguments, so its length
is 2:
const fn = (a, cb) => {};
console.log(fn.length); // 2
It is rarely used, but some libraries depend on it having the correct value, such as memoizee, which determines how to cache the function call or marked, a Markdown compiler, to decide whether its configuration is called async or sync.
While the length of the function is rarely used, it can cause problems. util.promisify
does not change it, so the resulting function will have the same
length as the original one, even though it needs fewer arguments.
console.log(fn.length); // 2
const promisified = util.promisify(fn);
console.log(promisified.length); // 2
Convert Promises to callbacks
The other direction is to have a Promise-returning function (usually an async
function) and you need to convert it to a callback-based one. It is much
rarer than the other way around, but there are cases when it's needed, usually when a library expects an asynchronous function and it supports only callbacks.
For example, the marked library supports a highlight
option that gets the code block and returns a formatted version. The highlighter gets the code, the
language, and a callback argument, and it is expected to call the last one with the result.
import marked from "marked";
import util from "util";
const promisifiedMarked = util.promisify(marked);
const res = await promisifiedMarked(md, {
// highlight gets a callback
highlight: (code, lang, cb) => {
const result = "Code block";
cb(null, result);
}
});
As with the promisification, there is a universal and a Node-callback-specific way to convert an async function to use callbacks. The universal way is to use an
async IIFE (Immediately-Invoked Function Expression) and add use then
to interface with the callback when there is a result or an error:
highlight: (code, lang, cb) => {
(async () => {
const result = "Code block";
return result;
})().then((res) => cb(null, res), (err) => cb(err));
}
This structure allows all callback styles as you control how a result or an error is communicated with the callback function.
For Node-style callbacks, you can use the util.callbackify
function that gets an async function and returns a callbackified version:
highlight: util.callbackify(async (code, lang) => {
const result = "Code block";
return result;
})
This yields a convenient structure and it is suitable in most cases.
Also, it changes the resulting function's length
by one, as it needs a callback as well as all the original arguments:
console.log(fn.length) // 2
console.log(util.callbackify(fn).length) // 3
Conclusion
Promises and async functions are the present, but still many things support only callbacks. While there are many ways to implement callbacks, the Node-style pattern is prevalent in Javascript. This allows utility functions to convert between Promises and callbacks mostly automatically.