DEV Community

Cover image for Practical Async JavaScript
Bogdan Varlamov
Bogdan Varlamov

Posted on

Practical Async JavaScript

Introduction

Let's take a moment to brush up on some JavaScript async concepts together.

What is Asynchronous?

First off, let's remind ourselves what an asynchronous process is. Simply put, it's a process that doesn't execute immediately but has a delay or lag. It's like ordering a coffee at a busy café - you place your order, and it takes a bit of time before you get your cup.

Creating a Fake API Call

Alright, can we create a function that mimics a basic API call?
Absolutely! Here's a simple example:

function fakeApiCall(response) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Success: ${response}`);
    }, 1000);
  });
}
Enter fullscreen mode Exit fullscreen mode

Our fakeApiCall function takes a value and returns it after a 1-second delay, labeled with "Success." We're using a Promise because it always handles asynchronous code, and setTimeout to manage the delay.

Adding Error Handling

But hey, do API calls always return the expected value? Nope, sometimes they return errors. Let's add this functionality too:

function fakeApiCall(response) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        resolve(`Success: ${response}`);
      } else {
        reject('API call failed: try again');
      }
    }, 1000);
  });
}
Enter fullscreen mode Exit fullscreen mode

Here, we've added a condition using Math.random(). Now, there's a 50% chance of success and a 50% chance of failure, mimicking real-world API behavior.

Retrying on Failure

Great! But what if we want to retry the API call when it fails? Let's tackle that:

let attempt = 0;
async function makeApiCallWithRetries(value) {
  try {
    const result = await fakeApiCall(value);
    return result;
  } catch (error) {
    attempt++;
    console.log(`Attempt ${attempt} failed: ${error} - for value: ${value}`);
    return makeApiCallWithRetries(value);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this code, we retry until we get a successful response.
Note, this could lead to an infinite loop if we're unlucky, but we'll hope for the best!

Setting a Retry Limit

To avoid that infinite loop, let's set a maximum retry limit:

let attempt = 0;
const maxRetries = 5;

async function makeApiCallWithRetries(value) {
  try {
    const result = await fakeApiCall(value);
    return result;
  } catch (error) {
    attempt++;
    if (attempt >= maxRetries) {
      throw new Error(`Max retries reached: ${error}`);
    }
    console.log(`Attempt ${attempt} failed: ${error} - for value: ${value}`);
    return makeApiCallWithRetries(value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, we'll stop trying after five attempts, avoiding the dreaded infinite loop.

Making Parallel API Calls

So, what if we need to make multiple API calls simultaneously?
Let's think this through. Imagine we have a bunch of requests that need to go out all at once. How can we handle that efficiently?
Well, we can run them in parallel. Here's how:

async function makeApiCallsInParallel(values) {
  const promises = values.map((value) => makeApiCallWithRetries(value));

  const results = await Promise.allSettled(promises);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Response ${index + 1} succeeded: ${result.value}`);
    } else {
      console.log(`Response ${index + 1} failed: ${result.reason}`);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

This function takes an array of values and calls makeApiCallWithRetries for each value in parallel. We're using Promise.allSettled to handle both successful and failed calls.

Conclusion

I hope this refreshed your memory about core JavaScript async functionality or even taught you something new.

Thanks for hanging out! Hit subscribe for more! 👋

Top comments (17)

Collapse
 
oculus42 profile image
Samuel Rouse

Nice article! I always enjoy seeing articles about promises and async JavaScript!

On the "retries" logic, though, you've created a single, global attempt counter. I would suggest a slight change to passing the values into the function. This keeps it isolated in scope and also uses good patterns when dealing with recursion. Something like this, perhaps:

async function makeApiCallWithRetries(value, maxRetries = 5, attempt = 1) {
  try {
    const result = await fakeApiCall(value);
    return result;
  } catch (error) {
    if (attempt >= maxRetries) {
      throw new Error(`Max retries reached: ${error}`);
    }
    console.log(`Attempt ${attempt} failed: ${error} - for value: ${value}`);
    return makeApiCallWithRetries(value, maxRetries, attempt + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Using default values allows them to be auto-populated on the first call and then incremented only within the recursive call, and it allows us to specify different maxRetries for different APIs, in case some are very flaky.

Collapse
 
bgdnvarlamov profile image
Bogdan Varlamov

Yes, that's a great suggestion. In production code, I would definitely implement it as you demonstrated, with a default value
Thanks!

Collapse
 
gauravchandra profile image
Gaurav Chandra

this is a great article. however, there is an opinion in the dev community that we should not be using async/await as it blocks the thread. Instead we should use observables. Care to shed a light on this?

Collapse
 
oculus42 profile image
Samuel Rouse

Async/await is non-blocking. They are literally a different way of writing promises, and if it help you can think of Promises as roughly equivalent to observables that are guaranteed to will only ever produce one event. They cover different use cases, but they are async.

Observables may be used to initiate promises, e.g. the classic RxJS Wikipedia search demo from MIX11, but the promise is still non-blocking

I would appreciate it if you have any links to information on this topic, in case I am misunderstanding or missing context.

Collapse
 
deepakbaid profile image
deepakbaid97

Using await is what blocks the thread. I think that's what he meant

Thread Thread
 
brandon_4ac813c611a5340fe profile image
Brandon

Seconding what @oculus42 said: unless there's a misunderstanding or missing context, this is still untrue.

Await is not blocking. It defers execution of subsequent code until after the awaited code completes, but it doesn't block the thread.

Collapse
 
lisacee profile image
Lisa Cee

I love the idea of using Math.random to simulate success and error states. I often have to prototype UI before an API is ready and this works better than having to write two functions and comment out the state I don't want.

Collapse
 
jangelodev profile image
João Angelo

Hi Bogdan Varlamov,
Thanks for sharing

Collapse
 
developedbyjk profile image
developedbyjk

A Perfect Place to Revise Quick!

Collapse
 
shinny22 profile image
Shinny22

Thanks that help me to understand async use in React.

Collapse
 
respect17 profile image
Kudzai Murimi

Insightful

Collapse
 
lotfijb profile image
Lotfi Jebali

Insightful

Collapse
 
schemetastic profile image
Schemetastic (Rodrigo)

Very useful!

Collapse
 
madhan_rock_9ffa73f834eb6 profile image
Madhan Rock

Clean and concise ✌️

Collapse
 
joo_carlosrovedaostrov profile image
João Carlos Roveda Ostrovski • Edited

Wouldn't be a better practice to use Promise.all or Promise.allSettled?

Refering to the last example in the article.

Collapse
 
vutunglaminfo profile image
Vu Tung Lam • Edited

Promise.all and Promise.allSettled both accept an array of Promise objects and return a single Promise object upon success. So what does the returned Promise resolve to, eventually?

If you use Promise.all then the returned Promise would resolve to an array of values which the input promises resolve to, respectively. And it would reject if ANY of the input promises rejects.

On the other hand, if you use Promise.allSettled then the returned Promise would ALWAYS resolve to an array of objects indicating whether each individual input promise resolves or not ; and if it resolves, what the resolved value is.

So you use Promise.all when you want all the input promises to succeed (resolve). And you would use Promise.allSettled when you want to inspect which input promises succeeded and which failed.

To further consolidate your understanding: Promise.allSettled returns a Promise that will always succeed/resolve. Why? You may want to reread what the returned Promise resolves to when you use Promise.allSettled, as mentioned above.

Collapse
 
joo_carlosrovedaostrov profile image
João Carlos Roveda Ostrovski

Thank you!