Asynchronous JavaScript
Moisés Fernández Zárate
Posted on December 8, 2022
JavaScript is a single threaded language, which means that it can only handle one process at a time, and it executes the code sequentially because it cannot run another instruction until it finishes executing the current instruction. This becomes a problem whenever tasks that are long-running tasks must be executed, such as HTTP requests or handling device settings, but this is when asynchronous programming comes in handy.
What is Asynchronous Programming?
This is a technique that allows JavaScript to handle long-running tasks and run other events in parallel, instead of having to wait for the first task to be finished, and this has been done using callbacks, promises and recently async/await.
What is a callback?
A callback is any function that is passed as an argument inside another function and then is invoked from within that function to perform a task. A good example to show this is with the use of setTimeout()
as shown below.
function printHelloWorld() {
console.log('Hello World');
}
setTimeout(printHelloWorld, 5000);
This function prints 'Hello World' after 5 seconds because of the callback it received. This is a simple example, but sometimes it becomes more complicated as in the example shown below.
function firstFunction(callback) {
console.log('This is the first message');
setTimeout(callback, 2000);
}
function secondFunction() {
console.log('This is the second message');
}
setTimeout(firstFunction, 5000);
setTimeout(firstFunction(secondFunction), 5000);
setTimeout(() => firstFunction(secondFunction), 5000);
The first call of setTimeout()
prints 'This is the first message' after five seconds but doesn't execute the setTimeout() inside the firstFunction()
because it didn't receive the callback.
The second call of setTimeout()
prints 'This is the first message' immediately because by opening up the parenthesis, it stopped being a callback and then prints 'This is the second message' after 2 seconds.
The third call of setTimeout()
prints 'This is the first message' after five seconds because we made it back a callback with the arrow function, and then prints 'This is the second message' after 2 seconds.
There were some cases where multiple calls were made in the code with many callbacks nested as in the example shown above, which had bad code readability and was not easy to main, this is called Callback Hell, but this was solved with the introduction of Promises.
What is a Promise?
A Promise in JavaScript is a special object which represents the eventual completion or failure of an asynchronous operation and its resulting value.
A Promise object has the following two properties:
- PromiseStatus
- PromiseValue
A Promise can only have one of the following three states:
- pending - initial state
- fulfilled - it means that the operation was completed successfully
- rejected - it means that the operation failed
When creating a Promise, two arguments must be sent as parameters, the first one is resolve
and the second one reject
, which are in charge of changing the PromiseStatus to fulfilled or rejected respectively.
const promise = new Promise(function(resolve, reject) {
resolve('Hello World');
});
The methods then
and catch
are used to handle the two possible states of a promise, the first one can be used to handle both fulfillments and rejections, and the second one only to handle rejections, but usually both are used combined because of code readability as shown in the example below.
function sumPositiveNumbers(number1, number2) {
const promise = new Promise((resolve, reject) => {
if (number1 < 0 || number2 < 0) {
reject('Both arguments must be positive numbers');
}
const result = number1 + number2;
resolve(result);
});
return promise;
}
sumPositiveNumbers(1, 2)
.then(value => {
console.log(value);
});
// Expected output: 3
sumPositiveNumbers(-1, 1)
.then(value => {
console.log(value);
return sumPositiveNumbers(-1, 1);
}, reason => {
console.log('Error: ' + reason);
});
// Expected output:
// Error: Both arguments must be positive numbers
sumPositiveNumbers(5, 5)
.then(value => {
console.log(value);
return sumPositiveNumbers(1, -5);
})
.catch(error => {
console.log('Error: ' + error);
});
// Expected output: 10
// Error: Both arguments must be positive numbers
When many promises are nested as in callbacks, a Promise Hell can be created and affect code readability and maintainability, but this can be solved by using the method all()
. This method can be used when the promises don't depend on each other and returns a fulfilled promise only if all promises where executed successfully and a rejected promise otherwise.
function firstPromiseFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('Cody'), 2000);
});
}
function secondPromiseFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('Bruno'), 2000);
});
}
function thirdPromiseFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => reject('Database error.'), 2000);
});
}
Promise.all([firstPromiseFunction(), secondPromiseFunction()])
.then((responses) => {
console.log(responses);
})
.catch((error) => {
console.error('Error ' + error);
});
// Expected output: ["Cody", "Bruno"]
Promise.all([firstPromiseFunction(), secondPromiseFunction(), thirdPromiseFunction()])
.then((responses) => {
console.log(responses);
})
.catch((error) => {
console.error('Error: ' + error);
});
// Expected output: Error: Database error.
Async and Await
These new keywords added in the ES6 Version made it simpler to work with asynchronous programming and promises in JavaScript. By adding the async
keyword JavaScript interprets the function as an asynchronous function. Inside an asynchronous function, the keyword await
can be used before a call to a function that returns a promise as in the example shown below.
const dogsDatabase = [
{
id: 1,
name: 'Cody',
age: 5,
breed: 'Siberian Husky'
},
{
id: 2,
name: 'Timo',
age: 12,
breed: 'Poodle'
},
{
id: 3,
name: 'Bruno',
age: 3,
breed: 'Golden Retriever'
},
];
function resolveAfter2Seconds() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Here is the dogs information:');
resolve(dogsDatabase);
}, 2000)
})
}
async function getDogs() {
console.log('Sending request to get dogs information.');
const result = await resolveAfter2Seconds();
console.log(result);
}
getDogs();
// Expected output:
// Sending request to get dogs information.
// Here is the dogs information:
// * The list of dogs *
The standard way to handle errors with they keywords async/await
is by using the block try/catch
.
function sumPositiveNumbers(number1, number2) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (number1 < 0 || number2 < 0) {
reject('Both numbers must be positive.')
}
resolve(number1 + number2)
}, 2000)
})
}
async function getSum(number1, number2) {
let result;
try {
console.log('Sending request to sum numbers.');
result = await sumPositiveNumbers(number1, number2);
console.log('The result is: ' + result);
} catch(error) {
console.log(error);
}
}
getSum(-1, 5);
// Expected output:
// Sending request to sum numbers.
// Both numbers must be positive.
Posted on December 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.