Promises and Async Await
Why some work does not finish immediately, what a promise represents, async and await, and the forgotten await that makes a function return before its work is done.
Why some work does not finish immediately, what a promise represents, async and await, and the forgotten await that makes a function return before its work is done.
Your program reads a file and prints its contents. It prints
undefined, then a moment later the file arrives and nothing
uses it. The read did not fail. It had not finished when the
next line ran.
Some work does not complete immediately — reading a file, asking another service, waiting for a reply. Everything so far in this course has assumed the next line runs after the previous one finished. By the end of this lesson you will know what happens when that assumption breaks, how to write code that waits, and why one missing keyword produces exactly the bug above.
const contents = readFile("photos.json");
console.log(contents); // not the contentsReading a file takes milliseconds — an eternity for a processor. Rather than stopping everything, the language starts the read and carries on, handing you a placeholder for the answer.
That placeholder is a Promise: an object representing a value that is not here yet.
const promise: Promise<string> = readFile("photos.json");Promise<string> means "a string, later". It is always in one of
three states.
pending
The work is running. This is the state your placeholder is in the moment you receive it.
fulfilled
It succeeded, and carries a value. await hands you that
value.
rejected
It failed, and carries an error. await throws that error at
the line where you waited.
The important thing about the model: one thing runs at a time. Your code is not interrupted halfway through a line. When it reaches a point where it must wait, the waiting is registered and the program continues; the rest of that work resumes later. So concurrency here is about waiting efficiently, not about several pieces of your code running at once.
await waits for a promise and gives you the value inside it.
contents is a string, not a Promise<string> — the type
reflects that.
Two rules come with it. await may only appear inside a
function marked async. And an async function always
returns a promise, whatever you write inside:
Writing Promise<string> as the return type is correct and what
the checker infers. async function getName(): string is an
error.
Bad — a forgotten await.
Good — waiting, or waiting for all of them.
The first version starts every save and immediately reports success. The saves may complete afterwards, or fail with nobody listening, and the function has already told its caller everything is done. If the program exits — a script, a serverless function — the writes are lost entirely.
Nothing errors. This is the most common bug in asynchronous
code, and the checker catches it only sometimes: turn on
no-floating-promises in your linter, which flags a promise
nobody awaited.
Two shapes, and the difference is usually the whole performance story.
Each takes a second, so the pair takes two. That is correct when
b needs a, and wasteful when it does not.
Both start immediately; the await waits for the slower. One
second.
The distinction the loops lesson raised now matters:
One at a time, deliberately.
What you want when the far end has a rate limit, or when the order matters, or when each step depends on the last.
All at once, and wait for the slowest.
What you want when they are independent. Two one-second operations take one second instead of two.
Starts everything, waits for nothing.
Reports success before any of it has finished, and loses every failure. Never correct.
Three related helpers:
allSettled is the one for "upload all of these and tell me
which failed":
That is a discriminated union, narrowed by status — the
pattern from the unions lesson, in the standard library.
A rejected promise behaves like a thrown error at the await:
Everything from the errors lesson applies — the caught value is
unknown, narrow with instanceof, re-throw what you did not
plan for.
finally works too, and is where cleanup belongs:
The failure that has no equivalent in synchronous code:
An unhandled rejection. In Node this crashes the process by
default; in a browser it appears in the console and nothing
else. Either way the error did not travel up through your code,
because nothing was waiting for it — the same missing await
from earlier, now losing an error rather than a result.
Before async existed, promises were used with .then(), and
you will meet it in existing code and in some APIs:
Each returns a new promise, so they chain. It is equivalent to
await with try/catch and considerably harder to read once
there is any branching, because the sequence lives in callbacks
rather than in statements.
Use await. Know .then for reading other people's code, and
for the one place it is genuinely tidier — attaching a handler
without waiting:
The void operator marks that as intentional, which is what
stops a linter flagging it.
Your programs can now wait for work that finishes later, run
independent work together, and handle failures that arrive after
the function that started them has returned. The single habit to
form is awaiting every promise or marking deliberately that you
are not — the missing await is the most common bug in this
part of the language.
Next is Modules, Import, and Export, which addresses the file your whole program lives in. It covers splitting code across files, what an import path actually resolves to, and keeping the dependencies between your files pointing one way.
Before you move on, write a function that waits for two slow
operations sequentially, time it, then rewrite it with
Promise.all and time it again. Then remove one await and
watch the function report success before the work is done. Both
take five minutes and neither is convincing until you have seen
it.
THE MODEL
a Promise<T> is a value that is not here yet
pending -> fulfilled with a value, or rejected with an error
ONE thing runs at a time; awaiting frees the program to
continue, it does not run your code in parallel
WAITING
async function f(): Promise<T> always returns a promise
const value = await promise; T, not Promise<T>
await only inside async
THE BUG
save(photo); starts it, does not wait
await save(photo); waits
a missing await reports success before the work is done,
and loses the error if it fails
turn on no-floating-promises in your linter
SEQUENTIAL VS TOGETHER
const a = await f(); const b = await g(); 2 seconds
const [a, b] = await Promise.all([f(), g()]); 1 second
for (const x of xs) await f(x); one at a time, deliberate
await Promise.all(xs.map(f)); all at once
xs.map(async x => await f(x)); <- waits for NOTHING
Promise.all rejects on the first failure
Promise.allSettled never rejects; each result reports itself
Promise.race the first to settle, either way
FAILURE
a rejection behaves like a throw at the await
try / catch / finally work as normal; caught value is unknown
an unawaited rejection is UNHANDLED: crashes Node, silent in a browser
nothing times out on its own:
fetch(url, { signal: AbortSignal.timeout(10_000) })
OLDER STYLE
.then(...).catch(...).finally(...)
equivalent, harder to read with branching
void promise.catch(() => {}) deliberately not awaitedasync function showPhotos(): Promise<void> {
const contents = await readFile("photos.json");
console.log(contents); // the actual contents
}async function getName(): Promise<string> {
return "Ana"; // wrapped in a promise for you
}async function saveAll(photos: Photo[]): Promise<void> {
for (const photo of photos) {
save(photo); // starts, does not wait
}
console.log("All saved"); // prints immediately
}async function saveAll(photos: Photo[]): Promise<void> {
await Promise.all(photos.map((photo) => save(photo)));
console.log("All saved");
}const a = await fetchAlbum(1); // waits
const b = await fetchAlbum(2); // then startsconst [a, b] = await Promise.all([fetchAlbum(1), fetchAlbum(2)]);for (const photo of photos) {
await upload(photo); // one at a time - deliberate
}
await Promise.all(photos.map((p) => upload(p))); // all at oncePromise.all([...]); // all succeed, or reject on the first failure
Promise.allSettled([...]); // never rejects; each result reports itself
Promise.race([...]); // the first to settle, success or failureconst results = await Promise.allSettled(photos.map(upload));
for (const result of results) {
if (result.status === "rejected") {
console.warn("failed:", result.reason);
}
}try {
const contents = await readFile("photos.json");
process(contents);
} catch (error) {
if (error instanceof Error) {
console.error("could not read:", error.message);
}
}setLoading(true);
try {
await save(photo);
} finally {
setLoading(false); // runs on success and on failure
}async function save(photo: Photo): Promise<void> {
throw new Error("disk full");
}
save(photo); // nobody awaits, nobody catchesconst response = await fetch(url, {
signal: AbortSignal.timeout(10_000),
});readFile("photos.json")
.then((contents) => process(contents))
.catch((error) => console.error(error))
.finally(() => setLoading(false));void logAnalytics(event).catch(() => {}); // deliberately not awaited