Map known failures to stable HTTP error responses instead of leaking raw exceptions.
Course navigationPhase 13⌄
Professional Error Handling
Create a stable application error model that separates expected domain/transport failures from unexpected faults and keeps diagnostics observable.
Phase outcome and project increment
Learning outcome
Create a stable application error model that separates expected domain/transport failures from unexpected faults and keeps diagnostics observable.
Concrete deliverable
Error taxonomy + NestJS mapping layer/filters + structured error response with request IDs.
Principles to keep while coding
Keep unexpected errors observable to operators while returning safe messages to clients.
Use consistent error shapes so frontend code does not branch on dozens of unrelated formats.
Preserve enough context in logs to diagnose an error without sending secrets to the browser.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Inventory current TaskFlow failures and group them into validation, auth, forbidden, missing, conflict, infrastructure, and unexpected categories.
- 02
Create explicit application errors for expected business outcomes rather than throwing arbitrary strings.
- 03
Map errors to one stable HTTP envelope at a central boundary.
- 04
Add request/correlation IDs to responses and logs.
- 05
Trigger each error class intentionally and verify both client-safe response and server diagnostic context.
- 06
Prove an unexpected exception returns a generic 500 while the real stack/context remains in logs.
Failures you should know how to diagnose
Returning raw exception messages/stacks
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Catching everything inside each controller
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Using one generic 400 for semantically different failures
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Logging secrets/tokens with error context
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Error Taxonomy and API Contract
Error Taxonomy & API Contract Expected failures, unexpected faults, HTTP semantics, machine codes, and public error design Phase 13 - Professional Error Handling Volume 04 - Professional Bac
NestJS Exception Layer and Request Lifecycle
NestJS Exception Layer & Request Lifecycle How filters fit into NestJS 12 request processing and why global error boundaries beat repetitive try/catch Phase 13 - Professional Error Handling
Design the TaskFlow Error Envelope
Design the TaskFlow Error Envelope statusCode, code, message, details, requestId, timestamp, and path Phase 13 - Professional Error Handling Volume 04 - Professional Backend Architecture mil
Request IDs and Structured Logging
Request IDs & Structured Logging Use Fastify request IDs, response headers, timing, and NestJS 12 structured logger metadata Phase 13 - Professional Error Handling Volume 04 - Professional B
Global ApiExceptionFilter
Global ApiExceptionFilter Build a Fastify-compatible catch-all filter with HttpAdapterHost and safe normalization Phase 13 - Professional Error Handling Volume 04 - Professional Backend Arch
Prisma Error Translation
Prisma Error Translation Translate Prisma 7 P2002, P2003, and P2025 without leaking ORM internals Phase 13 - Professional Error Handling Volume 04 - Professional Backend Architecture milesto
Service vs Filter Boundaries
Service vs Filter Boundaries Domain-specific exceptions, contextual translation, and keeping business meaning out of global infrastructure code Phase 13 - Professional Error Handling Volume
Safe Production Error Exposure
Safe Production Error Exposure Sanitized client responses, protected diagnostics, logging hygiene, and privacy-aware failure handling Phase 13 - Professional Error Handling Volume 04 - Profe
Client Error Consumption and Debugging
Client Error Consumption & Debugging Parse the stable envelope in Next.js and distinguish API failures from transport failures Phase 13 - Professional Error Handling Volume 04 - Professional
Hands On Lab and Phase Exit
Hands-On Lab & Phase Exit Error matrix, correlation exercise, engineering challenges, oral exam, and completion checklist Phase 13 - Professional Error Handling Volume 04 - Professional Back
Guided Build Lab
Purpose Create a stable application error model that separates expected domain/transport failures from unexpected faults and keeps diagnostics observable.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 13 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 13 without following a transcript.
Exercises that increase independence
Inventory current TaskFlow failures and group them into validation, auth, forbidden, missing, conflict, infrastructure, and unexpected categories.
Map errors to one stable HTTP envelope at a central boundary.
reproduce and diagnose — Returning raw exception messages/stacks.
deliver Error taxonomy + NestJS mapping layer/filters + structured error response with request IDs.
Can distinguish expected vs unexpected errors and Can map failures centrally.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_Error_Handling_Snapshot/README.txt
TaskFlow Phase 13 - Professional Error Handling This snapshot contains the files changed by Phase 13. It assumes the Phase 12 project already exists. Main additions: - one consistent API error response shape - a catch-all NestJS exception filter using HttpAdapterHost - Prisma P2002/P2003/P2025 translation - domain-specific not-found exceptions - Fastify request IDs surfaced to clients and logs - NestJS 12 structured logging metadata - a small Next.js-side API error parser Run apps/api, then execute tests/error-contract-smoke-tests.ps1.
reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
import { ApiExceptionFilter } from "./common/errors/api-exception.filter";
import { RequestLoggingInterceptor } from "./common/interceptors/request-logging.interceptor";
import { HealthModule } from "./health/health.module";
import { PrismaModule } from "./prisma/prisma.module";
import { TasksModule } from "./tasks/tasks.module";
@Module({
imports: [
HealthModule,
PrismaModule,
TasksModule,
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: RequestLoggingInterceptor,
},
{reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/common/errors/api-error-response.ts
export type ApiErrorDetails = string[] | Record<string, unknown>;
export type ApiErrorResponse = {
statusCode: number;
code: string;
message: string;
requestId: string;
timestamp: string;
path: string;
details?: ApiErrorDetails;
};reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/common/errors/api-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";
import { FastifyRequest } from "fastify";
import { Prisma } from "../../generated/prisma/client";
import { ApiErrorResponse } from "./api-error-response";
@Catch()
export class ApiExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(ApiExceptionFilter.name);
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/common/errors/application.exceptions.ts
import {
ConflictException,
NotFoundException,
} from "@nestjs/common";
export class TaskNotFoundException extends NotFoundException {
constructor(taskId: string) {
super({
code: "TASK_NOT_FOUND",
message: `Task ${taskId} was not found.`,
});
}
}
export class ProjectNotFoundException extends NotFoundException {
constructor(projectId: string) {
super({
code: "PROJECT_NOT_FOUND",
message: `Project ${projectId} was not found.`,
});reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/common/interceptors/request-logging.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from "@nestjs/common";
import { FastifyReply, FastifyRequest } from "fastify";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
@Injectable()
export class RequestLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLoggingInterceptor.name);
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const http = context.switchToHttp();reference/TaskFlow_Error_Handling_Snapshot/apps/api/src/tasks/tasks.service.ts
import { Injectable } from "@nestjs/common";
import { Prisma } from "../generated/prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import {
ProjectNotFoundException,
TaskNotFoundException,
} from "../common/errors/application.exceptions";
import {
CreateTaskDto,
UpdateTaskDto,
} from "./tasks.schemas";
@Injectable()
export class TasksService {
constructor(private readonly prisma: PrismaService) {}
findAll() {
return this.prisma.task.findMany({
orderBy: { createdAt: "desc" },
});reference/TaskFlow_Error_Handling_Snapshot/apps/web/src/lib/api/api-error.ts
export type ApiErrorResponse = {
statusCode: number;
code: string;
message: string;
requestId: string;
timestamp: string;
path: string;
details?: string[] | Record<string, unknown>;
};
export class ApiClientError extends Error {
constructor(
public readonly statusCode: number,
public readonly code: string,
message: string,
public readonly requestId?: string,
public readonly details?: ApiErrorResponse["details"],
) {
super(message);
}reference/TaskFlow_Error_Handling_Snapshot/tests/error-contract-smoke-tests.ps1
# TaskFlow Phase 13 error-contract smoke tests
# Run while apps/api is running on http://localhost:4000
$base = "http://localhost:4000"
function Show-ErrorContract {
param([scriptblock]$Request)
try {
& $Request
}
catch {
$response = $_.Exception.Response
if ($null -eq $response) {
Write-Host "No HTTP response was returned."
throw
}
$body = $_.ErrorDetails.Message | ConvertFrom-Json