Phase 1 · Node.js FoundationsModule 7~50 min read

Async Foundations, Events & Errors

Write reliable non-blocking programs with callbacks, promises, async/await, events, timers, and deliberate error handling.

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.

ConceptWhat it meansDecision rule
PromiseRepresents one eventual result or failureAwait it or return it so rejection remains connected to the caller
ConcurrencyMultiple operations make progress during overlapping timeStart independent I/O together; await dependent steps in order
EventEmitterPublishes named events to synchronous listenersUse for in-process one-to-many notifications with an explicit event contract
Error boundaryA layer that can add context or choose recoveryCatch only where you can recover, translate, or clean up

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Mark which operations are independent and which depend on previous results.
  2. Return or await every promise; never create a floating asynchronous failure.
  3. Catch errors at a boundary that can add useful context or select a response.
  4. Preserve the original error as cause when translating it.
  5. Name events in the past tense and document listener payloads.
  6. Test rejection, timeout, duplicate-event, and partial-success paths.

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

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.

load-config.js
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.

task-events.js
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.

repository.js
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

Wrapping await in try/catch and then silently returning undefined converts a visible failure into corrupted downstream state. Recover with a valid fallback or rethrow with context.

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

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

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