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?
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).
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
wait(1000).then(() => console.log('one second later'))fetch returns a promise too:
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.
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
// 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:
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:
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
- Forgetting await.
const data = loadQuestions()gives you a pending promise, not data. - await inside forEach.
array.forEach(async (x) => await save(x))doesn't wait. Usefor...of(one by one) orPromise.all(array.map(save))(parallel). - Unhandled rejections. Every promise chain needs a
catchor a surroundingtry/catch. - Making everything sequential. Independent requests should run in parallel.
// 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 oncePractice 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.
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.
Related posts
Web
CSS Flexbox vs Grid: When to Use Which (with Layouts You Will Actually Build)
A practical comparison of Flexbox and CSS Grid. Build a navbar, a card grid, a holy-grail page layout and a responsive dashboard, and learn a simple rule for choosing.
3 min read