Loading...
Loading...
Marco Franssen

In the JavaScript world, many APIs and libraries require you to write asynchronous logic using callbacks. With ES6, also referred to as ES2015, we gained the native Promise API. At the time of writing, most modern browsers and current Node.js releases support it, so we can use Promises without third-party libraries.
In Node.js, many libraries and built-in components use error-first callbacks, also referred to as errbacks. An errback has an error parameter first, followed by parameters for any data the API returns. These callbacks are a common way to handle asynchronous operations in Node.js and JavaScript. Below is an example of an errback used with the Mongoose connect function.
mongoose.connect("mongodb://localhost/my-db", function (err, db) {
if (err) {
console.error(`Unable to connect to database, Error: ${err}`);
} else {
console.log("Succesfully connected to database");
// here we can do anything with the db parameter.
}
});Callbacks can lead to code that gets more and more deeply nested—a situation often called callback hell. Below is an example of this pyramid-shaped code. Imagine adding even more method calls with callbacks. Can you picture how big the pyramid would get?
function validateToken(username, token, callback) {
openDatabase(function (db) {
getCollection(db, "user_tokens", function (err, collection) {
if (err) {
callback(err);
} else {
find(collection, { user: username }, function (err, result) {
if (err) {
callback(err);
}
callback(null, result.token === token);
});
}
});
});
}
validateToken("marco", "abc123def456", function (err, valid) {
if (err) {
console.error("Sigh, something went wrong! Error: " + err);
} else {
if (valid) {
console.log("awesome your token is valid!");
} else {
console.log("Nononoooo, that token is invalid!");
}
}
});We can avoid this kind of nesting by using Node.js modules and named functions, as described in this article. I won't cover that further here, as it's outside the scope of this post. Instead, I'm going to show you how we can use Promises to improve the code above.
With Promises, we can write the code as follows. Imagine the openDatabase method from the earlier example returns a Promise, along with the other asynchronous methods it calls. That lets us chain the operations like this.
validateToken(user, token) {
return openDatabase()
.then(function (db) {
return getCollection(db, 'user_tokens');
}).then(function (collection) {
return find(collection, { user: username });
}).then(function (result) {
return result.token === token;
});
});
validateToken('marco', 'abc')
.then(function (valid) {
if (valid) {
console.log('awesome your token is valid!');
} else {
console.log('Nononoooo, that token is invalid!');
}
})
.catch(function (error) {
console.error('Sigh, something went wrong! Error: ' + err);
})We can make this even cleaner with arrow functions, which also arrived with ES6, a.k.a. ES2015.
validateToken(user, token) {
return openDatabase()
.then(db => getCollection(db, 'user_tokens'))
.then(collection => find(collection, { user: username }))
.then(result => result.token === token);
});
validateToken('marco', 'abc')
.then(valid => {
if (valid) {
console.log('awesome your token is valid!');
} else {
console.log('Nononoooo, that token is invalid!');
}
})
.catch(error => console.error('Sigh, something went wrong! Error: ' + err));An arrow function with a single expression returns that expression's value implicitly, so you don't need the return keyword or curly braces. This means less syntax clutter. Much more readable, right? As you can see, though, the asynchronous functions we're calling also need to return Promises. We've seen how this can improve our code, but we haven't yet covered how to wrap an existing callback API in a Promise. First, let's take a look at what a Promise actually is.
A Promise represents the eventual result of an asynchronous operation. The operation may still be running, or the Promise may already be settled.
new Promise(/* executor */function (resolve, reject) { ... });The Promise constructor invokes the executor immediately, allowing your asynchronous logic to start. When that logic finishes or encounters an error, you call resolve with the result or reject with the error. A Promise has one of three states: pending, fulfilled, or rejected.
The Promise prototype provides two methods here: Promise.prototype.then and Promise.prototype.catch. Both return a Promise, making it possible to chain method calls.
var asyncOperation = new Promise(function (resolve, reject) {
// Some async stuff...
if (successfull) {
resolve(result);
} else {
reject(err);
}
});
asyncOperation
.then(doA)
.then(doB)
.catch(handleError)
.then(continueAfterError)
.then(doC)
.then(doD)
.catch(catchRemainingErrors);Let's take a look at how we can make the openDatabase method from the earlier example return a Promise instead of using a callback.
function openDatabase() {
return new Promise(function (resolve, reject) {
mongoose.connect("mongodb://localhost/my-db", function (err, db) {
if (err) {
reject(err);
} else {
resolve(db);
}
});
});
}At the time of writing, more and more libraries support Promises by default, so you won't always need the pattern above. However, some libraries still use the callback/errback approach. You can wrap those APIs in your own methods that return Promises. Let me give you one more example using the fs module.
function readFile(filePath) {
return new Promise((resolve, reject) =>
fs.readFile(filePath, (err, data) => (err ? reject(err) : resolve(data)))
);
}
readFile("/etc/hosts")
.then((data) => console.log(data))
.catch((err) => console.error(err));If you'd rather let a library do the wrapping, have a look at Bluebird. It provides convenient utility methods for creating Promise-returning versions of callback-based functions. See below for an example.
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs")); //Promisify all methods on the object
const redis = Promise.promisifyAll(require("redis")); //Promisify all methods on the object
const readFile = Promise.promisify(require("fs").readFile); //Promisify a single methodReferences:
With all this in mind, there's nothing stopping you from using Promises in both the browser and Node.js. If you need to support an older browser, you can use a library such as Bluebird. I'd be happy to hear your feedback in the comments below.
Marco Franssen
Package a static Angular app in an Nginx Docker image, build it, and run it with port and log volume mappings in this historical 2016 walkthrough.
Marco Franssen
Set up a Docker development environment on Windows or Mac using Docker Machine and VirtualBox, then configure your shell and run a test container.
Marco Franssen
Use the original Let's Encrypt client in a Vagrant VM to request an Azure website certificate through a reverse proxy, then export and upload a PFX.
Marco Franssen
Set up separate Windows 10 boot profiles with bcdedit to switch between Hyper-V for the Windows Phone emulator and VirtualBox for Linux development.