Lesson 4 of 1013 min
Asynchronous JavaScript
Networks and disks are slow. Learn how JavaScript starts slow work, keeps going, and picks the result up later with promises and async/await.
In this lesson
- Explain why JavaScript does not wait for slow work
- Create and consume promises
- Write sequential and parallel code with async/await
Why waiting is a problem#
JavaScript runs your code on a single thread. If it stopped and waited for a network response, the whole page would freeze: no scrolling, no clicks. Instead, slow work is started, JavaScript carries on, and the result is handled later. The mechanism that queues those "later" pieces is the event loop.
console.log("1: start");setTimeout(() => console.log("3: timer finished"), 0);1console.log("2: end of script");Even with a delay of 0 ms, the callback waits until the current script has finished. That is why "2" prints before "3".
Promises: a value that arrives later#
A promise represents the eventual result of slow work. It is pending at first, then either fulfilled with a value or rejected with an error.
function wait(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms));1}wait(500).then(() => console.log("half a second later"));You call resolve when the work is done. Most of the time you will use promises returned by APIs like fetch rather than create them.
async and await#
await pauses an async function until a promise settles, without blocking anything else. Code reads top to bottom again.
type Forecast = { city: string; celsius: number };async function getForecast(city: string): Promise<Forecast> { const url = `https://api.example.com/forecast?city=${encodeURIComponent(city)}`; const response = await fetch(url);1 if (!response.ok) throw new Error(`Forecast failed: HTTP ${response.status}`);2 const data = (await response.json()) as Forecast;3 return data;}fetch returns a promise; await gives you the response once headers arrive.
fetch only rejects on network failure. A 404 or 500 still "succeeds", so check response.ok yourself.
Reading the body is slow too, so json() also returns a promise. The as Forecast tells TypeScript what shape we expect; a later lesson shows how to check it properly.
Note
api.example.com is a placeholder domain. Swap in any real JSON API to run this.
Sequential or parallel#
Awaiting one call after another is simple but slow when the calls do not depend on each other. Promise.all starts them together and waits for all of them.
function wait(ms: number): Promise<string> { return new Promise((resolve) => setTimeout(() => resolve(`${ms} ms`), ms));}async function sequential() { const a = await wait(300);1 const b = await wait(300); return [a, b]; // about 600 ms}async function parallel() { const [a, b] = await Promise.all([wait(300), wait(300)]);2 return [a, b]; // about 300 ms}console.log(await sequential(), await parallel());The second wait does not start until the first has finished.
Both timers start at once. If any promise rejects, Promise.all rejects immediately.
Promise.all or Promise.allSettled?
Promise.all fails fast: one rejection and you lose the other results, which is right when you need everything. Promise.allSettled waits for every promise and reports each outcome, which suits dashboards where one failing widget should not blank the rest.
Handling failure#
A rejected promise becomes a thrown error inside an async function, so ordinary try/catch works.
async function loadProfile(id: string): Promise<string> { try { const response = await fetch(`https://api.example.com/users/${id}`); if (!response.ok) return "Profile unavailable"; const user = (await response.json()) as { name: string }; return user.name; } catch { return "You seem to be offline"; }}Warning
Forgetting await is the most common async bug: you get a pending promise instead of the value, and errors escape your try/catch. Editors and TypeScript's lint rules can flag "floating" promises.
ExercisePredict the order
What does this print, and in what order?
console.log("A");setTimeout(() => console.log("B"), 0);Promise.resolve().then(() => console.log("C"));console.log("D");Show solution
A, D, C, B. The synchronous logs run first. Promise callbacks (microtasks) run before timer callbacks (tasks), so C comes before B.
Recap#
- Slow work runs in the background; the event loop schedules what happens next.
- Promises represent future values;
async/awaitmakes them read like ordinary code. - Use
Promise.allfor independent work andtry/catchfor failures, and always checkresponse.ok.