DEV Community

Cover image for Several recommended practices for writing good asynchronous JavaScript code
Safdar Ali
Safdar Ali

Posted on

Several recommended practices for writing good asynchronous JavaScript code

Introduction

Asynchronous programming in JavaScript is crucial for building efficient and responsive web applications. However, improper use of asynchronous constructs can lead to performance issues and hard-to-debug errors. Here are several best practices to help you write better asynchronous JavaScript code.

Avoid Using async in the Promise Constructor

Bad Practice

// ❌
new Promise(async (resolve, reject) => {});


Enter fullscreen mode Exit fullscreen mode

Good Practice

// ✅
new Promise((resolve, reject) => {});

Enter fullscreen mode Exit fullscreen mode

Explanation:

Using async within the Promise constructor can lead to unnecessary wrapping of Promises. Additionally, if an async function inside the Promise constructor throws an exception, the constructed Promise will not reject, making it impossible to catch the error. Instead, use a regular function and handle the asynchronous operations within it.

Avoid await Inside Loops

Bad Practice

// ❌
for (const url of urls) {
  const response = await fetch(url);
}

Enter fullscreen mode Exit fullscreen mode

Good Practice

// ✅
const responses = [];
for (const url of urls) {
  const response = fetch(url);
  responses.push(response);
}
await Promise.all(responses);

Enter fullscreen mode Exit fullscreen mode

Explanation:
Using await inside a loop can prevent JavaScript from fully leveraging its event-driven nature. This approach executes fetch requests sequentially, which can be slow. Instead, initiate all fetch requests concurrently and use Promise.all to wait for their completion, significantly improving execution efficiency.

Do Not Return From the Promise Constructor Function

It is not recommended to return a value from the Promise constructor function, as the value returned will be ignored and can cause confusion in the codebase.

Bad Practice


// ❌
new Promise((resolve, reject) => {
  return someValue;
});

Enter fullscreen mode Exit fullscreen mode

Good Practice

// ✅
new Promise((resolve, reject) => {
  resolve(someValue);
});

Enter fullscreen mode Exit fullscreen mode

Explanation:
Returning a value from the Promise executor function does not have any effect. Instead, use resolve to fulfill the promise with the intended value.

Use try...catch for Error Handling in async Functions

Proper error handling is critical in asynchronous code to ensure that your application can gracefully handle failures.

Good Practice

async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
    // Handle the error appropriately
  }
}

Enter fullscreen mode Exit fullscreen mode

Explanation:
Using try...catch blocks within async functions allows you to handle errors gracefully, ensuring that unexpected issues do not crash your application.

Avoid Creating Unnecessary Promises

Creating unnecessary promises can add complexity and reduce the readability of your code.

Bad Practice

// ❌
async function example() {
  return new Promise(async (resolve) => {
    const result = await someAsyncFunction();
    resolve(result);
  });
}

Enter fullscreen mode Exit fullscreen mode

Good Practice

// ✅
async function example() {
  return await someAsyncFunction();
}

Enter fullscreen mode Exit fullscreen mode

Explanation:
If you are already working within an async function, there is no need to wrap another async function in a Promise. The async keyword automatically returns a promise.

Conclusion

By adhering to these best practices, you can write more efficient, readable, and maintainable asynchronous JavaScript code. Avoid using async within Promise constructors, refrain from using await inside loops, ensure proper error handling, and prevent the creation of unnecessary promises. These guidelines will help you leverage the full potential of JavaScript’s event-driven architecture, resulting in better-performing applications.

That's all for today.

And also, share your favourite web dev resources to help the beginners here!

Connect with me:@ LinkedIn and checkout my Portfolio.

Explore my YouTube Channel! If you find it useful.

Please give my GitHub Projects a star ⭐️

Thanks for 22525! 🤗

Top comments (3)

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

I would change some things from that post tbh

// 😵
const responses = [];
for (const url of urls) {
  const response = fetch(url);
  responses.push(response);
}
await Promise.all(responses);
Enter fullscreen mode Exit fullscreen mode

for:

// ✅
const responses = [];
for (const url of urls) 
  responses.push( fetch(url) );

Promise.allSettled(responses).then( data => doWhateverWithThe(data) );
Enter fullscreen mode Exit fullscreen mode

Here it comes the explanation:

Avoiding the await

The await keyword breaks the async and makes the runtime effectively wait till every promise is done (bad yada yada) it's way better to let it be and send a callback to do whatever when they decide to resolve or reject.

Here's a quick example you may've seen in React:

Promise.allSettled(responses).then( data => setComponents( [...components, data]) );
Enter fullscreen mode Exit fullscreen mode

Promise.all vs Promise.allSettled

Following the previous point, both Promise.all and Promise.allSettled "convert" an iterable (usually an array) of promises into a single promise which will behave different depending on the API you use;

If you use Promise.all the entire promise rejects whenever a single one of the promises fails.
We usually don't want that. E.G. when loading data for 6 components, if a single component failed or timed out or whatever I'd rather get the other 5 working and show an error in just one of them.
In these common situations Promise.allSettled comes to help. It will only reject if all executed promises reject, otherwise it will resolve (then control the unhappy path in the component instead).

Other changes:

On the other hand, this:

async function example() {
  return await someAsyncFunction();
}
Enter fullscreen mode Exit fullscreen mode

Will break the async nature of it, so I don't know how can it be a good practice! ❌ This code is effectively calling an async function just to make it wait till another thing finishes, making it synchronous 💀

It also breaks your advice of using Promise.resolve instead of return 😅 is this post generated with AI?

There are several ways to fix that like sending a function as reference and executing it as a callback once the async thingy has resolved or rejected;

async function example( action ) {
    someAsyncFunction().then( x => action(x) );
}
Enter fullscreen mode Exit fullscreen mode

side note: you might also want to Promise.resolve() that in a way or another depending on your app logic.

!important

Please, I know that wrapping your head around asynchronous stuff is complicated but double check the statements (both using the logic notation you learn in the college/uni and/or by simply running your code) so other newbies don't get wrong advice.

As a rule of thumb, whenever you find yourself converting async code in a sync execution, you're doing something wrong.

We have state and other tools in React which you can use for control flow asynchronously (not to mention new sweets from React 19).
Solid JS has it probably even better with Signals, which are now available in Angular as well. In vanilla JS you may use object proxies to check if a variable has changed or not, making it easy to run other code when it gets updated by an asynchronous process...

Best,
Joel

Collapse
 
safdarali profile image
Safdar Ali

Thanks Joe for sharing your knowledge with us. Appreciate it!!!!

Collapse
 
safdarali profile image
Safdar Ali

Thanks for 22525! 🤗