In synchronous programming, all operations happen in a sequence with each operation firing after the result of the previous one is obtained. When a function is called that performs a long-running action, the rest of the program has to stop and wait for it to return before continuing. This can be highly problematic in JavaScript because we are frequently dealing with user-interfaces and servers. User inputs and server requests are unpredictable, and we need our programs to continue to perform operations while waiting on their results. Because JavaScript is single-threaded and synchronous by default, we have to use a few special tools if we want to perform these asynchronous tasks. Two of these tools are async callbacks and promises.
Callbacks
The original way of dealing with this dilemma is with asynchronous callbacks. A callback is basically a function passed as an argument to another function to be executed upon the completion of an asynchronous operation. These callback functions can either be defined functions or anonymous functions created on the fly, but typically they need to be able to either handle an error, passed in as their first argument, or pass on their collected data or result.
In this example we are passing the callback function as the last parameter in getStatusCode
, and then we define it anonymously on the next line passing it err
and the expected data to be retrieved as data
. After request.get
completes the resulting data or error will be handled by our callback function.
This all seems pretty clean and simple, and it can be, but because we have to handle both the result and error possibilities in all of our callbacks, things can get pretty messy when dealing with nested callbacks. If you're not careful you could have something that looks like this 'callback hell' pattern -
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function (err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
Promises
Introduced in 2015 with ES6, promises offer us a cleaner and more efficient way of handling asynchronous code than their predecessor the callback. A promise is an object that contains a callback function carrying both resolve
and reject
outcomes. This allows us to use the promises' provided methods .then()
and .catch()
to handle what happens upon success or failure rather than having to pass in a callback function.
Here we construct getFiles
to return a new promise. If there is an error, the promise will reject. If there is no error the promise resolves, and we are given a promise object with .then()
and .catch()
methods. Now when we call createFile
, because it returns a promise, we no longer pass it a callback. Instead we can chain on .then()
and .catch()
to perform the respective success or fail responses. It may not seem like much on this small scale, but this allows us to chain on many .then()
calls followed by a single .catch()
rather than falling into a nested callback pattern.
Top comments (0)