What you'll learn
Node.js is designed around work that finishes later. You will connect callbacks, promises, async/await, timers, events, and errors into one reliable model instead of treating them as unrelated syntax.
By the end of this lesson, you'll be able to:
- Read callback-, promise-, and async/await-based APIs
- Run independent work concurrently without losing errors
- Create and consume domain events with EventEmitter
- Classify expected operational failures and unexpected programmer defects
Core mental model
Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.
| Concept | What it means | Decision rule |
|---|---|---|
| Promise | Represents one eventual result or failure | Await it or return it so rejection remains connected to the caller |
| Concurrency | Multiple operations make progress during overlapping time | Start independent I/O together; await dependent steps in order |
| EventEmitter | Publishes named events to synchronous listeners | Use for in-process one-to-many notifications with an explicit event contract |
| Error boundary | A layer that can add context or choose recovery | Catch only where you can recover, translate, or clean up |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Mark which operations are independent and which depend on previous results.
- Return or await every promise; never create a floating asynchronous failure.
- Catch errors at a boundary that can add useful context or select a response.
- Preserve the original error as cause when translating it.
- Name events in the past tense and document listener payloads.
- Test rejection, timeout, duplicate-event, and partial-success paths.
Keep the feedback loop short
Guided code lab
Run independent reads concurrently
Both reads start before awaiting the pair. Promise.all is right when the combined result is useless if either operation fails.
import { readFile } from "node:fs/promises";
async function loadJson(url) {
const text = await readFile(url, "utf8");
return JSON.parse(text);
}
const [settings, features] = await Promise.all([
loadJson(new URL("./settings.json", import.meta.url)),
loadJson(new URL("./features.json", import.meta.url)),
]);
console.log({ settings, features });Publish a domain event
Listeners run synchronously in registration order. The emitter owns the event name and payload contract.
import { EventEmitter } from "node:events";
const taskEvents = new EventEmitter();
taskEvents.on("taskCreated", (task) => {
console.log("Audit: created task", task.id);
});
taskEvents.on("taskCreated", (task) => {
console.log("Notify owner:", task.owner);
});
taskEvents.emit("taskCreated", { id: 7, owner: "Maya" });Translate an error without losing its cause
The repository adds domain context while preserving the low-level error for logs and debugging.
import { readFile } from "node:fs/promises";
export async function loadCatalog(fileUrl) {
try {
return JSON.parse(await readFile(fileUrl, "utf8"));
} catch (error) {
throw new Error("Catalog could not be loaded", { cause: error });
}
}
try {
await loadCatalog(new URL("./missing.json", import.meta.url));
} catch (error) {
console.error(error.message);
console.error("Cause code:", error.cause?.code);
}Production practice
Own every promise
Await, return, combine, or explicitly observe each promise. Unhandled rejections are defects, not a control-flow strategy.
Bound concurrency
Do not launch an unbounded number of file or network operations from arbitrary input; apply batching or a concurrency limit.
Log context
Record the operation and safe identifiers once at the handling boundary; avoid duplicate logs at every layer.
Common failure mode
Independent workshop
Build a batch profile loader that reads several JSON files concurrently, reports every success and failure, and emits an event for each valid profile.
Your finished workshop must include:
- Promise.allSettled or an equivalent deliberate partial-success strategy
- Event payloads with stable documented shapes
- Translated errors retaining cause
- No unhandled promise rejections
- A final success/failure summary
Definition of done
Recap & quick check
Key takeaways
- Promises represent eventual outcomes
- async functions always return promises
- Independent I/O can overlap
- EventEmitter supports synchronous in-process notifications
- Catch errors only where you can make a decision
- Error causes preserve diagnostic context
Quick check
1. When is Promise.all appropriate?
2. How are EventEmitter listeners invoked by emit?
3. What should happen to a promise created inside a function?
4. Why attach an error cause?
Next: Phase Project: Build a Task Tracker CLI