DEV Community

Orbit Websites
Orbit Websites

Posted on

Understanding Async/Await in JavaScript: A Practical Guide

Understanding Async/Await in JavaScript: A Practical Guide

If you’ve ever had to fetch data from an API, read a file, or wait for a timer, you’ve run into asynchronous code. And if you’ve tried to handle that with callbacks or promises, you know how quickly it can get messy. Async/await isn’t magic — but it does make async code look and behave more like the synchronous code we’re used to. Let’s cut through the noise and talk about how to actually use it.


What Is Async/Await?

async/await is syntactic sugar over JavaScript’s existing promise system. It doesn’t change how promises work — it just makes them easier to read and write.

An async function always returns a promise. Inside it, await pauses execution until the promise settles (resolves or rejects). That’s it.

async function getData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

This looks clean. No .then(), no nested callbacks. But under the hood, it’s still promise-based.

💡 Pro tip: await only works inside async functions. Try using it elsewhere, and you’ll get a syntax error.


Error Handling: Try/Catch, Not .catch()

With .then().catch(), error handling is chained. With async/await, use try/catch.

async function fetchUserData() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) throw new Error('Network error');
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Fetch failed:', error);
    // Handle or rethrow
  }
}
Enter fullscreen mode Exit fullscreen mode

This is more familiar if you’re used to synchronous error handling. But don’t forget: if you don’t catch, the error becomes an unhandled rejection.

⚠️ Always handle errors. Even if it’s just logging them.


You Can’t “Partially” Await

One common gotcha: await is all or nothing per statement.

// This waits for each sequentially
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
Enter fullscreen mode Exit fullscreen mode

That’s fine if one depends on the other. But if they’re independent, you’re adding unnecessary latency.

To run them in parallel, use Promise.all():

async function loadUserAndPosts() {
  try {
    const [user, posts] = await Promise.all([
      fetch('/api/user').then(r => r.json()),
      fetch('/api/posts').then(r => r.json())
    ]);
    return { user, posts };
  } catch (error) {
    console.error('One of the requests failed', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now both requests fire at the same time. Much faster.

✅ Rule of thumb: if promises don’t depend on each other, run them in parallel.


Don’t Forget: Async Functions Return Promises

This trips people up:

async function getUsername() {
  const user = await fetch('/api/user').then(r => r.json());
  return user.name;
}

// This doesn't give you the name directly
const name = getUsername(); // ❌ This is a promise!
Enter fullscreen mode Exit fullscreen mode

You still have to await or .then() the result:

getUsername().then(name => console.log(name));
// or
const name = await getUsername(); // inside another async function
Enter fullscreen mode Exit fullscreen mode

🧠 Remember: async functions don’t return values — they return promises that resolve to values.


Avoiding Common Anti-Patterns

1. await on Every Line (When You Don’t Need To)

// ❌ Slower than it needs to be
const response = await fetch('/api/data');
const data = await response.json();
return data;
Enter fullscreen mode Exit fullscreen mode

You can’t parallelize response.json() because it depends on fetch. But this is still fine — just don’t assume every await is a performance hit.

2. Wrapping Everything in async

// ❌ Not needed
async function logMessage(msg) {
  console.log(msg);
}

// ✅ Just a normal function
function logMessage(msg) {
  console.log(msg);
}
Enter fullscreen mode Exit fullscreen mode

async has overhead. Only use it when you need await or are returning a promise.


Real-World Example: Retrying a Failed Request

Here’s a practical use case: retrying a flaky API call.

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(response.status);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error; // Last try failed
      console.log(`Retry ${i + 1} after error:`, error);
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // Exponential backoff
    }
  }
}

// Usage
fetchWithRetry('/api/data')
  .then(data => console.log('Success:', data))
  .catch(err => console.error('Failed after retries:', err));
Enter fullscreen mode Exit fullscreen mode

This is hard to write cleanly with .then(). With async/await, it’s readable and maintainable.


When Not to Use Async/Await

Sometimes


Community-Focused

Top comments (0)