WorryFree Computers   »   [go: up one dir, main page]

Getting to know asynchronous JavaScript: Callbacks, Promises and Async/Await

Sebastian Lindström
CodeBuddies
Published in
11 min readJul 2, 2017

--

Introduction

If you are new to JavaScript it can be hard to understand its asynchronous nature. In this article I will try my best to explain it.

Asynchrony in JavaScript

According to Wikipedia: Asynchrony in computer programming refers to the occurrence of events independently of the main program flow and ways to deal with such events.

In programming languages like e.g Java or C# the “main program flow” happens on the main thread or process and “the occurrence of events independently of the main program flow” is the spawning of new threads or processes that runs code in parallel to the “main program flow”.

This is not the case with JavaScript. That is because a JavaScript program is single threaded and all code is executed in a sequence, not in parallel. In JavaScript this is handled by using what is called an “asynchronous non-blocking I/O model”. What that means is that while the execution of JavaScript is blocking, I/O operations are not. I/O operations can be fetching data over the internet with Ajax or over WebSocket connections, querying data from a database such as MongoDB or accessing the filesystem with the NodeJs “fs” module. All these kind of operations are done in parallel to the execution of your code and it is not JavaScript that does these operations; to put it simply, the underlying engine does it.

Callbacks

For JavaScript to know when an asynchronous operation has a result (a result being either returned data or an error that occurred during the operation), it points to a function that will be executed once that result is ready. This function is what we call a “callback function”. Meanwhile, JavaScript continues its normal execution of code. This is why frameworks that does external calls of different kinds have APIs where you provide callback functions to be executed later on.

Registering event listeners in a browser with “addEventListener”, reading a files content with “fs.readFile” or registering a middleware in an express web server with “server.use” are examples of common APIs that uses callbacks.

Here is an example of fetching data from an URL using a module called “request”:

const request = require(‘request’);
request('https://www.somepage.com', function (error, response, body) {
if(error){
// Handle error.
}
else {
// Successful, do something with the result.
}
});

The following works just as fine and will give the same result as above:

const request = require(‘request’);function handleResponse(error, response, body){
if(error){
// Handle error.
}
else {
// Successful, do something with the result.
}
}
request('https://www.somepage.com', handleResponse);

As you can see, “request” takes a function as its last argument. This function is not executed together with the code above. It is saved to be executed later once the underlying I/O operation of fetching data over HTTP(s) is done. The underlying HTTP(s) request is an asynchronous operation and does not block the execution of the rest of the JavaScript code. The callback function is put on a sort of queue called the “event loop” until it will be executed with a result from the request.

Callback hell

Callbacks are a good way to declare what will happen once an I/O operation has a result, but what if you want to use that data in order to make another request? You can only handle the result of the request (if we use the example above) within the callback function provided.

In this example the variable “result” will not have a value when printed to the console at the last line:

const request = require(‘request’);
let result;
request('http://www.somepage.com', function (error, response, body) {
if(error){
// Handle error.
}
else {
result = body;
}
});
console.log(result);

The last line will output “undefined” to the console because at the time that line is being executed, the callback has not been called. Even if the request were somehow to complete before the result variable is printed to the console (highly unlikely though), this code will still run to completion before the callback is executed anyway because that is the nature of the non-blocking I/O model in JavaScript.

So if we want to do a second request based on the result of a first one we have to do it inside the callback function of the first request because that is where the result will be available:

request('http://www.somepage.com', function (firstError, firstResponse, firstBody) {
if(firstError){
// Handle error.
}
else {
request(`http://www.somepage.com/${firstBody.someValue}`, function (secondError, secondResponse, secondBody) {
if(secondError){
// Handle error.
}
else {
// Use secondBody for something
}
});
}
});

When you have a callback in a callback like this, the code tends to be a bit less readable and a bit messy. In some cases you may have a callback in a callback in a callback or even a callback in a callback in a callback in a callback. You get the point: it gets messy.

One thing to note here is the first argument in every callback function will contain an error if something went wrong, or will be empty if all went well. This pattern is called “error first callbacks” and is very common. It is the standard pattern for callback-based APIs in NodeJs. This means that for every callback declared we need to check if there is an error and that just adds to the mess when dealing with nested callbacks.

This is the anti-pattern that has been named “callback hell”.

Promises

A promise is an object that wraps an asynchronous operation and notifies when it’s done. This sounds exactly like callbacks, but the important differences are in the usage of Promises. Instead of providing a callback, a promise has its own methods which you call to tell the promise what will happen when it is successful or when it fails. The methods a promise provides are “then(…)” for when a successful result is available and “catch(…)” for when something went wrong.

There are lots of frameworks for creating and dealing with promises in JavaScript, but all of the examples below assumes that we are using native JavaScript promises as introduced in ES6.

Using a promise this way looks like this:

someAsyncOperation(someParams)
.then(function(result){
// Do something with the result
})
.catch(function(error){
// Handle error
});

One important side note here is that “someAsyncOperation(someParams)” is not a Promise itself but a function that returns a Promise.

The true power of promises is shown when you have several asynchronous operations that depend on each other, just like in the example above under “Callback Hell”. So let’s revisit the case where we have a request that depends on the result of another request. This time we are going to use a module called “axios” that is similiar to “request” but it uses promises instead of callbacks. This is also to point out that callbacks and promises are not interchangeable.

Using axios, the code would instead look like this:

const axios = require(‘axios’);axios.get(‘http://www.somepage.com')
.then(function (response) { // Reponse being the result of the first request
// Returns another promise to the next .then(..) in the chain
return axios.get(`http://www.somepage.com/${response.someValue}`);
})
.then(function response { // Reponse being the result of the second request
// Handle response
})
.catch(function (error) {
// Handle error.
});

Instead of nesting callbacks inside callbacks inside callbacks, you chain .then() calls together making it more readable and easier to follow. Every .then() should either return a new Promise or just a value or object which will be passed to the next .then() in the chain. Another important thing to notice is that even though we are doing two different asynchronous requests we only have one .catch() where we handle our errors. That’s because any error that occurs in the Promise chain will stop further execution and an error will end up in the next .catch() in the chain.

A friendly reminder: just like with callback based APIs, this is still asynchronous operations. The code that is executed when the request has finished — that is, the subsequent .then() calls — is put on the event loop just like a callback function would be. This means you cannot access any variables passed to or declared in the Promise chain outside the Promise. The same goes for errors thrown in the Promise chain. You must also have at least one .catch() at the end of your Promise chain for you to be able to handle errors that occur. If you do not have a .catch(), any errors will silently pass and fade away and you will have no idea why your Promise does not behave as expected.

To make this even clearer, this kind of error handling will not work at all with Promises:

try{
axios.get(‘http://www.somepage.com')
.then(function response {
// Handle response
})
} catch (error){
// We will never end up here, even if there is an error thrown inside the Promise chain
}

NodeJs will actually warn you if you omit a .catch() in your Promise chain by logging this:

UnhandledPromiseRejectionWarning: Unhandled promise rejection
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

This basically means that you should always have a .catch() and not having one will be deprecated in a future version of NodeJs.

Creating promises

As stated above, callbacks are not interchangeable with Promises. This means that callback-based APIs cannot be used as Promises. The main difference with callback-based APIs is it does not return a value, it just executes the callback with the result. A Promise-based API, on the other hand, immediately returns a Promise that wraps the asynchronous operation, and then the caller uses the returned Promise object and calls .then() and .catch() on it to declare what will happen when the operations has finished.

The creation of a Promise object is done via the Promise constructor by calling “new Promise()”. It takes a function as an argument and that function gets passed two callbacks: one for notifying when the operation is successful (resolve) and one for notifying when the operation has failed (reject). What you pass as an argument when calling resolve will be passed to the next then() in the promise chain. The argument passed when calling reject will end up in the next catch(). It is a good idea to make sure that you always pass Error objects when calling reject.

We can wrap a callback based asynchronous operation with a Promise like this:

function getAsyncData(someValue){
return new Promise(function(resolve, reject){
getData(someValue, function(error, result){
if(error){
reject(error);
}
else{
resolve(result);
}
})
});
}

Note that it is within the function being passed to the Promise constructor that we start the asynchronous operation. That function is then responsible for calling resolve(success) when it’s done or reject(error) if there are errors.

This means that we can use the function “getAsyncData” like this:

getAsyncData(“someValue”)
// Calling resolve in the Promise will get us here, to the first then(…)
.then(function(result){
// Do stuff
})
// Calling reject in the Promise will get us here, to the catch(…)
// Also if there is an error in any then(..) it will end up here
.catch(function(error){
// Handle error
});

The process of wrapping a callback based asynchronous function inside a Promise and return that promise instead is called “promisification”. We are “promisifying” a callback-based function. There are lots of modules that let you do this in a nice way but since version 8 NodeJs has a built in a helper called “util.promisify” for doing exactly that.

This means that our whole Promise wrapper above could instead be written like this:

const { promisify } = require(‘util’);const getAsyncData = promisify(getData);getAsyncData(“someValue”)
.then(function(result){
// Do stuff
})
.catch(function(error){
// Handle error
});

Async/Await

Async/Await is a language feature that is a part of the ES8 standard. It was implemented in version 7.6 of NodeJs. If you are new to JavaScript this concept might be a bit hard to wrap your head around, but I would advise that you still give it a try. You don’t have to use it if you don’t want to. You will be fine with just using Promises.

Async/Await is the next step in the evolution of handling asynchronous operations in JavaScript. It gives you two new keywords to use in your code: “async” and “await”. Async is for declaring that a function will handle asynchronous operations and await is used to declare that we want to “await” the result of an asynchronous operation inside a function that has the async keyword.

A basic example of using async/await looks like this:

async function getSomeAsyncData(value){
const result = await fetchTheData(someUrl, value);
return result;
}

The following is not a legal use of the await keyword since it can only be utilized inside a function with the async keyword in front of it:

function justANormalFunction(value){
// will result in a SyntaxError since async is missing on function declaration
const result = await fetchTheData(someUrl, value);
return result;
}

A function call can only have the await keyword if the function being called is “awaitable”. A function is “awaitable” if it has the async keyword or if it returns a Promise. Remember when I said that callbacks and Promises are not interchangeable and you have to wrap a callback based function inside a Promise and return that Promise? Well, functions with the async keyword are interchangeable with functions that returns Promises which is why I stated that a function that returns a Promise is “awaitable”.

That basically means that this will work:

function fetchTheData(someValue){
return new Promise(function(resolve, reject){
getData(someValue, function(error, result){
if(error){
reject(error);
}
else{
resolve(resutl);
}
})
});
}
async function getSomeAsyncData(value){
const result = await fetchTheData(value);
return result;
}

Also this will work:

async function getSomeData(value){
const result = await fetchTheData(value);
return result;
}
getSomeData(‘someValue’)
.then(function(result){
// Do something with the result
})
.catch(function (error){
// Handle error
});

Error handling with async/await

Inside the scope of an async function you can use try/catch for error handling and even though you await an asynchronous operation, any errors will end up in that catch block:

async function getSomeData(value){
try {
const result = await fetchTheData(value);
return result;
}
catch(error){
// Handle error
}
}

I stated before that you only need one .catch(..) at the end of a Promise chain even though you are doing several asynchronous calls in that chain. The same goes for async/await and error handling with try/catch. You only need to surround the code in the “first” async function with try catch. That function can await one or more other async functions which in return does their own asynchronous calls by awaiting one or more other async functions etc.

The following is a valid for handling errors in such a case:

async function fetchTheFirstData(value){
return await get("someUrl", value);
}
async function fetchTheSecondData(value){
return await getFromDatabase(value);
}
async function getSomeData(value){
try {
const firstResult = await fetchTheFirstData(value);
const result = await fetchTheSecondData(firstResult.someValue);
return result;
}
catch(error){
// Every error thrown in the whole “awaitable” chain will end up here now.
}
}

An important consideration regarding async/await

Async/await may make your asynchronous calls look more synchronous but it is still executed the same way as if it were using a callback or promise based API. The asynchronous I/O operations will still be processed in parallel and the code handling the responses in the async functions will not be executed until that asynchronous operation has a result. Also, even though you are using async/await you have to sooner or later resolve it as a Promise in the top level of your program. This is because async and await are just syntactical sugar for automatically creating, returning and resolving Promises.

Resources

More on the anti-pattern “Callback Hell”
http://callbackhell.com

MDN on Promises in JavaScript
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise

The Evolution of Asynchronous JavaScript by RisingStack
https://blog.risingstack.com/asynchronous-javascript

Understanding the NodeJs Event Loop by RisingStack
https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop

--

--