What are callbacks and promises?

To explain promises we must first explain callbacks. A callback function is simply a function you pass into another function so that function can call it at a later time. This is commonly seen in asynchronous APIs; the API call returns immediately because it is asynchronous, so you pass a function into it that the API can call when it's done performing its asynchronous task.
Callbacks
The simplest example I can think of in JavaScript is the setTimeout()
function. It's a global function that accepts two arguments. The first argument is the callback function and the second argument is a delay in milliseconds. The function is designed to wait the appropriate amount of time, then invoke your callback function.
setTimeout(function () {
console.log("10 seconds later...");
}, 10000);
You may have seen the above code before but just didn't realize the function you were passing in was called a callback function. We could rewrite the code above to make it more obvious.
var callback = function () {
console.log("10 seconds later...");
};
setTimeout(callback, 10000);
Callbacks are used all over the place in Node because Node is built from the ground up to be asynchronous in everything that it does. Even when talking to the file system. That's why a ton of the internal Node APIs accept callback functions as arguments rather than returning data you can assign to a variable. Instead it will invoke your callback function, passing the data you wanted as an argument. For example, you could use Node's fs
library to read a file. The fs
module exposes two unique API functions: readFile
and readFileSync
.
The readFile
function is asynchronous while readFileSync
is obviously not. You can see that they intend you to use the async calls whenever possible since they called them readFile
and readFileSync
instead of readFile
and readFileAsync
. Here is an example of using both functions.
Synchronous:
var data = fs.readFileSync('test.txt');
console.log(data);
The code above blocks thread execution until all the contents of test.txt
are read into memory and stored in the variable data
. In node this is considered very bad practice.
Asynchronous (with callback):
var callback = function (err, data) {
if (err) return console.error(err);
console.log(data);
};
fs.readFile('test.txt', callback);
First we create a callback function that accepts two arguments err
and data
. One problem with asynchronous functions is that it becomes more difficult to trap errors so a lot of callback-style APIs pass errors as the first argument to the callback function. It is best practice to check if err
has a value before you do anything else. If so, stop execution of the callback and log the error.
Synchronous calls have an advantage when there are thrown exceptions because you can simply catch them with a try/catch
block.
try {
var data = fs.readFileSync('test.txt');
console.log(data);
} catch (err) {
console.error(err);
}
In asynchronous functions it doesn't work that way. The API call returns immediately so there is nothing to catch with the try/catch
. Proper asynchronous APIs that use callbacks will always catch their own errors and then pass those errors into the callback where you can handle it as you see fit.
Promises
In addition to callbacks though, there is another popular style of API that is commonly used called the promise. Promises are a style of programming that allows us to flatten out asynchronous APIs so there aren't so many callbacks and nested callbacks. I think the best way to understand promises is to see them in action and then talk about them.
Let's say we want to load a file from the file system, then make a database call, then send an email. All of these things have asynchronous API calls and would look something like this:
fs.readFile('test.txt', function (err, data) {
if (err) return console.error(err);
db.insert({
fileName: 'test.txt',
fileContent: data
}, function (err, result) {
if (err) return console.error(err);
smtp.send({
to: 'test@test.com',
subject: 'test',
body: 'This is a test.'
}, function (err) {
if (err) return console.error(err);
console.log("Success email sent.");
});
});
});
This is what most people call "callback hell". It's hard to read and difficult to maintain. We could flatten this out a little with named callbacks:
var emailSent = function (err) {
if (err) return console.error(err);
console.log("Success email sent.");
};
var fileSaved = function (err, result) {
if (err) return console.error(err);
smtp.send({
to: 'test@test.com',
subject: 'test',
body: 'This is a test.'
}, emailSent);
};
var readFileDone = function (err, data) {
if (err) return console.error(err);
db.insert({
fileName: 'test.txt',
fileContent: data
}, fileSaved);
};
fs.readFile('test.txt', readFileDone);
It does reduce some of the nesting issues but I honestly don't think that is all that much easier to understand. We may be able to clean it up a bit more, even declare the functions in a more comprehensive order for readability. No matter what we do to clean it up a bit though, it is still a complicated mess. You have to check for errors every step of the way and if you want to avoid callback hell then you have to stop and brainstorm about an assortment of functions to define that you can use as your callbacks.
Let's do the same thing with promises shall we? We'll pretend that fs.readFile
, db.insert
, and smtp.send
all return promises.
fs.readFile('test.txt')
.then(function (data) {
return db.insert({
fileName: 'test.txt',
fileContent: data
});
})
.then(function (document) {
return smtp.send({
to: 'test@test.com',
subject: 'test',
body: 'This is a notification email about' + document.fileName + '.'
});
})
.then(function () {
console.log("Success email sent.");
}, function (err) {
console.error(err);
});
Without knowing how this magic is working it should still be pretty obvious how much better this is. The first and most obvious reason is that you no longer have callback hell, but as Domenic Denicola pointed out in his blog post, flattening out callbacks is just a happy side effect of promises. Promises do a lot more than make our code easier to read and organize; they also give us back control over our error handling. Did you notice in the above example that we only check for errors one single time?
A promise is simply an object that exposes a .then
function that takes two callbacks as arguments. The first callback is the done
or success
callback. The second callback is the fail
or error
callback. The nifty part about promises is that you only have to specify an error
callback on the very last call to .then
. If an error is thrown it will be passed up the chain to the final error handler. Promises essentially give us back the error handling that was robbed from us when we decided to embrace asynchronous code.
In the previous example we pretended that those functions were already coded to return promises; Now let's say those functions actually do use callbacks. In an effort to understand promises better let's find a promise library and use it to turn those callback APIs into promise APIs. There are lots of promise libraries already out there. One popular library is called BlueBird. We can use BlueBird to turn those callback-style functions into functions that use promises.
// promiseWrappers.js
var Promise = require('bluebird'),
fs = require('fs'),
db = require('./db'),
smtp = require('./smtp');
exports.fs = {
readFile: function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
});
}
};
exports.db = {
insert: function (doc) {
return new Promise(function (resolve, reject) {
db.insert(doc, function (err, result) {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
}
};
exports.smtp = {
send: function (msg) {
return new Promise(function (resolve, reject) {
smtp.send(msg, function (err) {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
};
We just created a module that exposes some promise-enabled wrappers around some APIs that do not understand promises, but use callbacks instead. Now we can use the promise-style script we looked at before and it will work as intended :D
var p = require('./promiseWrappers');
p.fs.readFile('test.txt')
.then(function (data) {
return p.db.insert({
fileName: 'test.txt',
fileContent: data
});
})
.then(function (document) {
return p.smtp.send({
to: 'test@test.com',
subject: 'test',
body: 'This is a notification email about + ' document.fileName + '.'
});
})
.then(function () {
console.log("Success email sent.");
}, function (err) {
console.error(err);
});
We created our wrappers for learning purposes, but if you've never used BlueBird before then you'll be excited to find that it actually has helper functions for wrapping node-style err, result
API calls. We don't even need to write our promiseWrappers.js
file. We can just modify our code to use BlueBird instead.
Promise.promisify(fs.readFile, fs)('test.txt')
.then(function (data) {
return Promise.promisify(db.insert, db)({
fileName: 'test.txt',
fileContent: data
});
})
.then(function () {
return Promise.promisify(smtp.send, smtp)({
to: 'test@test.com',
subject: 'test',
body: 'This is a test.'
});
})
.then(function () {
console.log("Success email sent.");
}, function (err) {
console.error(err);
});
If you know that the method you're trying to promisify doesn't refer to this
inside of it, then you don't need to supply a second argument to Promise.promisify
. However, if it does refer to this
or you're unsure, then you'll want to pass in the base object as the second argument. Passing that second argument is a BlueBird shortcut for this:
Promise.promisify(db.insert.bind(db));
The call to .bind
is a native JavaScript method on functions that returns a new function whose this
value is bound to it. In other words, if the insert
function refers to this
during its execution then it will still be referencing db
as it should.
Promises are even more fun when you have a bunch of asynchronous activities and none of them depend on the result of another. It means you can run them all in parallel and then just run some code when they are all finished. Most promise libraries expose helpers that allow you to do this easily. Even BlueBird does this.