Avoid common mistakes in Asynchronous JavaScript
In this blog post, we will learn some common mistakes while writing asynchronous javaScript code with examples. If you are a beginner, this article is for you, but even if you are an experienced developer, who knows, you might have missed something too. :)
I have listed some of the common mistakes while writing asynchronous javaScript code as follows:
1. Using forEach
for sequential promise execution #
Given we have a collection of promises as follows, we want to loop through each promise and wait for it to resolve before moving on to the next one.
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 3000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 500);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 2000);
});
const collection = [p1, p2, p3];
// mistake 1: using await in the forEach callback
async function execute() {
collection.forEach(async function (promiseItem) {
const value = await promiseItem;
console.log(value);
});
}
// mistake 2: using await in the forEach callback + forEach statement itself
async function execute() {
await collection.forEach(async function (promiseItem) {
const value = await promiseItem;
console.log(value);
});
}
// uncomment and run each function one by one
// execute()
But using any flavors of forEach
, all code above will output in the wrong order in the console as follows:
2
3
1
Note:
forEach
is not designed to work withasync
callbacks. It will not wait for theasync
callback to complete and will move on to the next iteration. This is the reason why we see the output in the console in the wrong order.
How can we fix this?
There are many ways to fix this. But we will look at two of them.
Alternative 1: for...of
// approach 1: using await in the for statement itself
async function execute() {
for await (const promiseItem of collection) {
console.log(promiseItem);
}
}
// approach 2: using await in the body of the loop
async function execute() {
for (const promiseItem of collection) {
console.log(await promiseItem);
}
}
// uncomment and run each function one by one
// execute()
Note: "The for
await...of
statement creates a loop iterating overasync
iterable objects as well as sync iterables." - MDN
Alternative 2: Array.prototype.reduce()
collection.reduce(async (prev, current) => {
// wait for the previous promise to resolve
await prev;
const value = await current;
console.log(value);
}, Promise.resolve());
Both alternative 1 and alternative 2 above will output in the console in the following correct order.
1
2
3
2. Silent failures #
Unhandled promise rejections is one of the sources of silent failures which is very painful to debug and track down the bugs. Let's see how we can avoid them.
We have a function called readFile
that reads a config file and returns a promise that resolves with the file’s contents.
This is a node.js example, but the same concept applies to the browser as well. For example, if we replace the fs.readFile
with some callback based API such as XMLHttpRequest
in the browser, the same problem will occur.
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "config.json");
const readFile = () => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
resolve(data);
});
});
};
readFile()
.then((data) => {
console.log("success:", data);
})
.catch((err) => {
console.log("error:", err);
});
What will happen if the config.json file somehow accidentally got deleted?
- The error will be silently ignored and the program will continue to run with undefined config state.
catch
block is never executed.- We will see the following output in the console.
success: undefined
How can we fix this?
We need to make sure to handle the error scenario. We can do this by adding a reject
call inside the callback as shown below.
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "config.json");
const readFile = () => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, data) => {
// added code to handle error scenario
if (err) {
reject(err);
}
resolve(data);
});
});
};
readFile()
.then((data) => {
console.log("success:", data);
})
.catch((err) => {
console.log("error:", err);
});
With this change, catch
block is executed, and we will see the following output in the console if the config.json file is deleted.
error: [Error: ENOENT: no such file or directory ....]
So, proper error handling is very important to avoid silent failures and debug nightmares.
3. Error handling on multiple promises #
While dealing with multiple promises, we need to make sure to handle the error scenario properly. Let's see how we can do that.
async function getData() {
const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
const p2 = new Promise((_, reject) =>
setTimeout(() => reject("error"), 500)
);
const results = [await p1, await p2];
return results;
}
getData().catch((err) => console.log("catch:", err));
The above code will throw the following unhandled promise rejection error in node.
catch
block will never get executed.
[UnhandledPromiseRejection: This error originated....]
Note:
Timer runs concurrently for both
p1
andp2
and doesn't wait for each other.
p2
waits forp1
to complete. Look at the order ofawait
in the array.Even if the order of wait is
p1
first and thenp2
, ifp2
rejects beforep1
is fulfilled, then an unhandled promise rejection error will trigger, irrespective of whether the caller has set up a catch clause or not.
How to fix this?
We can use Promise.all
instead of multiple await
in an array to solve this problem.
async function getData() {
const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
const p2 = new Promise((_, reject) =>
setTimeout(() => reject("error"), 500)
);
const results = await Promise.all([p1, p2]);
return results;
}
getData().catch((err) => console.log("catch:", err));
With this change, catch
block is executed, and we will see the following output in the console.
catch: error
4. Catching errors thrown from async callbacks #
We can’t catch errors thrown from async callbacks with a try/catch
block.
In the following example, setTimeout
is being used to simulate an asynchronous operation, but it can be any asynchronous operation such as reading a file, making an API call, etc. Callback function of setTimeout
is only called after the execution of try/catch
block because the event loop first executes the current call stack and then executes the callback queue afterwards. I will highly recommend learning more about event loop to understand the rationale behind this.
try {
setTimeout(() => {
throw "error occurred";
}, 0);
} catch (err) {
console.log("error:", err);
}
Note:
catch
block is never executed. You might be tempted to use application’s global uncaught exception handler such asprocess.on('uncaughtException')
event on node orwindow.onerror()
event on browser to catch these errors, but these are not meant to be used as a replacement for proper error handling and should be avoided.
5. Order of then and catch matters in promise chains #
Promise order matters and results in different behavior. This is even more important when we are dealing with a long chain of promises because in case of a bug, it will be difficult to find the bug in the code.
Let's imagine we have a function called getUsers
that returns a promise that resolves with the list of users in case of success and rejects when there is some api issue. And we have a function called render
that renders the list of users on the UI in case of success and logs the error in case of failure.
function getUsers() {
return new Promise((resolve, reject) => {
// simulate some api call error
reject("Network Error");
});
}
function render() {
getUsers()
.catch((err) => {
console.log("catch:", err);
})
.then((users) => {
console.log("success:", users);
});
}
render();
The above code will work fine if there is no error. We can replace the reject
with the resolve
in the getUsers
function to see the success scenario.
But what would happen if there is an error?
It will output the following in the console in case of an error.
catch: Network Error
success: undefined
We are seeing the success message in the console even though the api call failed. This is because then
block is executed even when there is an error. This might bring some unexpected behavior in the application.
Note:
catch
block is a shorthand forPromise.prototype.then(undefined, onRejected)
. This means it also returns apromise
and can be chained in the same way asthen
block.
How can we fix this?
We need to make sure to handle the error scenario after the then
block.
function getUsers() {
return new Promise((resolve, reject) => {
// simulate some api call error
reject("Network Error");
});
}
function render() {
getUsers()
// we have moved catch block after then block
.then((users) => {
console.log("success:", users);
})
.catch((err) => {
console.log("catch:", err);
});
}
render();
The above code will output the following in the console. then
block is never executed.
catch: Network Error
6. Classic newbie mistake #
function getConfig() {
let config;
// simulating some api call
setTimeout(() => {
config = {
name: "John",
age: 30,
};
}, 0);
return config;
}
const config = getConfig();
console.log(config);
The above code will always output undefined
in the console. This is because setTimeout
is an asynchronous operation and the getConfig
function returns config
before the setTimeout
callback is executed.
undefined
How to fix this?
There are multiple ways to fix this, but one of the ways is to use promise approach as follows:
function getConfig() {
return new Promise((resolve, reject) => {
// simulating some api call
setTimeout(() => {
const config = {
name: "John",
age: 30,
};
resolve(config);
}, 0);
});
}
getConfig().then((config) => {
console.log(config);
});
The above code will output the following in the console.
{
name: 'John',
age: 30
}
Closing Notes #
We learned about common mistakes while writing asynchronous javaScript code and the ways to fix it with examples. I hope you found this article useful. If you have any questions, please post them in the comments section below.
References
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function