What is Asynchronous JavaScript?

JavaScript runs in a single thread — it can only do one thing at a time. But many operations take time: fetching data from an API, reading a file, or waiting for a user action. Without async handling, the whole browser would freeze while waiting.

Asynchronous JavaScript lets you start a time-consuming operation, continue doing other things, and handle the result when it arrives — without blocking. Promises and async/await are the two modern tools for this.

What are Promises?

A Promise is an object that represents a value that will be available in the future. A promise can be in one of three states: pending (waiting), fulfilled (success), or rejected (failed).

You handle a fulfilled promise with .then() and a rejected one with .catch().

// Fetching data with Promises
fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => {
    console.log(data);          // use the data
  })
  .catch(error => {
    console.error('Error:', error);
  })
  .finally(() => {
    console.log('Done!');       // runs always
  });

// Creating your own Promise
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

delay(1000).then(() => console.log('1 second passed'));

Promise chains work but can get hard to read when you need to do many operations in sequence — this is why async/await was introduced.

What is Async/Await?

Async/await is syntactic sugar over Promises — under the hood, it still uses Promises. But it lets you write async code that looks and reads like synchronous code, which makes it much easier to understand.

  • async before a function means it will always return a Promise
  • await pauses execution inside the async function until the Promise resolves
// Same fetch operation with async/await
async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    const data = await response.json();
    console.log(data);          // use the data
  } catch (error) {
    console.error('Error:', error);
  } finally {
    console.log('Done!');
  }
}

getUsers();

await can only be used inside an async function. If you try to use it outside, you will get a syntax error.

Side-by-Side Comparison

FeaturePromises (.then)Async/Await
Syntax styleChained .then() and .catch()Reads like synchronous code
Error handling.catch()try/catch block
ReadabilityGets messy with many stepsVery clean, especially for sequences
DebuggingStack traces can be unclearBetter stack traces
Parallel executionPromise.all() is naturalawait Promise.all() is just as easy
Under the hoodUses PromisesUses Promises
When to preferSimple one-shot operationsSequential async steps, complex logic

Error Handling

This is where the two approaches look most different. With Promises you use .catch(); with async/await you use try/catch.

// Promises: error handling
fetch('/api/data')
  .then(res => res.json())
  .catch(err => console.error(err));

// Async/Await: error handling
async function getData() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error('Network error: ' + res.status);
    const data = await res.json();
    return data;
  } catch (err) {
    console.error(err);
  }
}

Always check response.ok when using the Fetch API. A 404 or 500 response does NOT trigger the .catch() or throw an error automatically — only network failures do.

When to Use Each

Use async/await when:

  • You have multiple async operations that depend on each other (sequential steps)
  • You want clean, readable code that others can easily maintain
  • You are working inside a function and want try/catch error handling
  • You are learning — async/await is generally easier to understand first

Use Promises (.then) when:

  • You are at the top level of a module (though modern JS supports top-level await)
  • You need to chain a simple one-step operation
  • You are working with Promise combinators like Promise.all, Promise.race, Promise.allSettled

Modern consensus: Use async/await by default for its readability. Use Promise combinators when you need parallel execution. They work together seamlessly.

Running Multiple Requests in Parallel

If you need to make multiple API calls that do NOT depend on each other, run them in parallel with Promise.all() — not sequentially with multiple awaits.

// SLOW: sequential (second waits for first to finish)
async function slow() {
  const users  = await fetch('/api/users').then(r => r.json());
  const posts  = await fetch('/api/posts').then(r => r.json());
  return { users, posts };
}

// FAST: parallel (both fire at the same time)
async function fast() {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  return { users, posts };
}

Use Promise.all() when requests are independent. Use sequential await when the second request depends on the result of the first.