Skip to content
Sign in

JavaScript Promises and async/await: From Confused to Confident

Understand how JavaScript handles waiting with the event loop, promises and async/await. Fetch real data, run requests in parallel, handle errors and avoid the classic mistakes.

CodeOrbit Learn TeamPublished 4 min read

JavaScript runs your code on one thread. Yet a web page can download data, wait for a timer and respond to clicks all at once. The secret is that JavaScript never waits: it hands slow work to the browser and continues, coming back when the result is ready. Promises and async/await are the tools that let us write that "come back later" code clearly.

What does this print?

JavaScript
console.log('1: start')
setTimeout(() => console.log('2: timer'), 0)
Promise.resolve().then(() => console.log('3: promise'))
console.log('4: end')

Output: 1: start, 4: end, 3: promise, 2: timer.

  • Normal code runs first, top to bottom.
  • Promise callbacks go into the microtask queue, which runs as soon as the current code finishes.
  • Timer callbacks go into the task queue, which runs after the microtasks.

This is the event loop. You don't need every detail, just this: callbacks never interrupt running code; they wait their turn.

Promises: a value that arrives later

A promise is an object representing a future result. It is pending, then either fulfilled (with a value) or rejected (with an error).

JavaScript
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

wait(1000).then(() => console.log('one second later'))

fetch returns a promise too:

JavaScript
fetch('https://api.example.com/questions')
  .then((response) => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return response.json()
  })
  .then((questions) => console.log(questions.length, 'questions'))
  .catch((error) => console.error('Could not load:', error.message))

Each .then returns a new promise, so steps chain. One .catch at the end handles an error from any step.

async/await: the same thing, easier to read

An async function always returns a promise. Inside it, await pauses that function until a promise settles, without blocking the page.

JavaScript
async function loadQuestions() {
  const response = await fetch('https://api.example.com/questions')
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

async function showQuestions() {
  try {
    const questions = await loadQuestions()
    render(questions)
  } catch (error) {
    showMessage('Could not load questions. Please try again.')
  } finally {
    hideSpinner()
  }
}

Sequential vs parallel

JavaScript
// Slow: waits for each request before starting the next (≈ 3 × 300 ms)
const user = await getUser()
const scores = await getScores()
const news = await getNews()

// Fast: all three start together (≈ 300 ms)
const [user2, scores2, news2] = await Promise.all([getUser(), getScores(), getNews()])

Use sequential await only when a step needs the previous result (for example, you need the user's id to fetch their scores).

The Promise combinators

Method

Resolves when

Rejects when

Use for

Promise.all

All succeed

Any one fails

Data you need all of

Promise.allSettled

All finish

Never

Dashboards where some widgets may fail

Promise.race

First settles

First settles with an error

Timeouts

Promise.any

First succeeds

All fail

Trying several mirrors

A timeout with race:

JavaScript
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Request timed out')), ms),
  )
  return Promise.race([promise, timeout])
}

const data = await withTimeout(fetch('/api/slow'), 5000)

Cancelling a request

When a user types in a search box, older requests become useless. AbortController cancels them:

JavaScript
let controller

async function search(term) {
  controller?.abort()                 // cancel the previous search
  controller = new AbortController()
  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(term)}`, {
      signal: controller.signal,
    })
    renderResults(await res.json())
  } catch (error) {
    if (error.name !== 'AbortError') showMessage('Search failed')
  }
}

Common mistakes

  1. Forgetting await. const data = loadQuestions() gives you a pending promise, not data.
  2. await inside forEach. array.forEach(async (x) => await save(x)) doesn't wait. Use for...of (one by one) or Promise.all(array.map(save)) (parallel).
  3. Unhandled rejections. Every promise chain needs a catch or a surrounding try/catch.
  4. Making everything sequential. Independent requests should run in parallel.
JavaScript
// Correct ways to process a list
for (const answer of answers) await saveAnswer(answer)        // in order
await Promise.all(answers.map((answer) => saveAnswer(answer))) // all at once

Practice task

Write loadDashboard() that fetches a profile, scores and announcements in parallel with Promise.allSettled, shows whatever succeeded, and displays a small warning for anything that failed.

Tags:#JavaScript#Web#Async

Frequently asked questions

Is async/await faster than .then()?

No, they are the same mechanism. async/await is just easier to read and makes try/catch error handling natural.

Can I use await outside a function?

Yes, in JavaScript modules (type="module" scripts and modern bundlers) top-level await is allowed. In classic scripts it must be inside an async function.

What is the difference between a microtask and a task?

Promise callbacks are microtasks and run right after the current code. setTimeout and events are tasks and run after all microtasks are done.