Node.js

Lesson 03

Async failure boundaries

Handle rejected promises at the boundary where you can return a useful response.

Good Code

src/routes/reports.ts
import type { ServerResponse } from "node:http";
import { createReport } from "../services/reports";

export async function sendReport(response: ServerResponse) {
  try {
    const report = await createReport();

    response.setHeader("content-type", "application/json");
    response.end(JSON.stringify({ report }));
  } catch (error) {
    console.error({ error }, "Could not create report");
    response.statusCode = 500;
    response.end(JSON.stringify({ error: "Could not create report." }));
  }
}

Bad Code

src/routes/reports.ts
import type { ServerResponse } from "node:http";
import { createReport } from "../services/reports";

export function sendReport(response: ServerResponse) {
  createReport().then((report) => {
    response.setHeader("content-type", "application/json");
    response.end(JSON.stringify({ report }));
  });
}

Review Notes

What to review

Good Code

The good version catches async failures at the HTTP boundary, logs context, and returns a single error response.

Bad Code

The bad version never handles a rejected promise, so the process can emit an unhandled rejection while the client waits or disconnects.

Takeaways

  • Every async request path should have a clear failure path and one response.