Async JavaScript
Scope: promises, async/await, and the event loop — the
mental model everything in Node is built on. All examples were
run with Node v24.
Why async exists
Reading a file or querying a database takes time — milliseconds to whole seconds. If Node waited doing nothing, one slow request would stall the entire server. So slow operations return immediately with an IOU, and Node gets on with other work until the result arrives. That IOU is a promise.
Promises
import { readFile } from "node:fs/promises";
const promise = readFile("hello.mjs", "utf8");
console.log("started reading...");
const text = await promise;
console.log("got", text.length, "characters");
A promise is in one of three states: pending (waiting),
fulfilled (got a value), or rejected (failed with an
error). await pauses your function until it settles — the
rest of Node keeps running meanwhile.
async/await: the modern way
Mark a function async to use await inside it:
async function readConfig(path) {
const text = await readFile(path, "utf8");
return JSON.parse(text);
}
const config = await readConfig("config.json");
An async function always returns a promise — even if you
return a plain value, callers get a promise of that value. So
await it (or .then() it) at the call site.
Doing things in parallel
Awaits run one after another by default:
const a = await readFile("a.txt", "utf8"); // waits...
const b = await readFile("b.txt", "utf8"); // ...then waits
For independent work, start both, then await both:
const pa = readFile("a.txt", "utf8"); // starts now
const pb = readFile("b.txt", "utf8"); // starts now too
const [a, b] = await Promise.all([pa, pb]); // waits for both
Promise.all fails fast: if any promise rejects, the whole
thing rejects.
.then()/.catch(): the older style
readFile("hello.mjs", "utf8")
.then((text) => console.log("got", text.length))
.catch((err) => console.error("failed:", err.message));
You'll read this in older code. Prefer async/await in new
code — it reads top-to-bottom instead of nesting sideways.
The event loop in one paragraph
Node runs your JavaScript on one thread. When you await
something slow, Node parks your function and runs other code.
When the slow thing finishes, your function resumes where it
left off. Consequences:
- Slow I/O (files, network) is cheap — thousands can overlap.
- Slow computation (a giant loop) blocks everything, because
there's no
awaitinside it for Node to park on. Keep heavy math out of the main thread.
Timers fit the same model:
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("one second later");
The await-in-loops trap
// SLOW: each iteration waits for the last
for (const f of files) {
await processFile(f);
}
// FAST: all start together
await Promise.all(files.map((f) => processFile(f)));
If the iterations are independent, Promise.all + map is the
idiom. (If order or rate-limiting matters, the slow loop is
correct — know which you need.)
Check
- [ ] You can explain pending/fulfilled/rejected in one sentence
- [ ] You know an
asyncfunction always returns a promise - [ ] You can parallelize independent awaits with
Promise.all - [ ] You can say what blocks the event loop and what doesn't
Next: Modules and npm.