Lesson 10 of 49 intermediate

Error Handling

Catching Problems Before They Crash

Open interactive version (quiz + challenge)

Real-world analogy

Error handling is like a safety net under a trapeze artist. The artist (your code) performs amazing feats, but sometimes things go wrong mid-air. Without a safety net (try/catch), they crash to the ground (app crashes). With it, they bounce back and the show goes on!

What is it?

Error handling is the practice of anticipating, detecting, and gracefully responding to errors in your code. Instead of letting your app crash, you catch errors and either recover from them, show helpful messages, or log them for debugging. In NestJS, exception filters provide a structured way to handle HTTP errors.

Real-world relevance

Every production API must handle errors properly. A payment processing endpoint that crashes instead of returning a clear error message could cost thousands of dollars. Good error handling is the difference between a professional and amateur application.

Key points

Code example

// Basic try/catch/finally
try {
  const data = JSON.parse('invalid json');
} catch (error) {
  console.error('Parse failed:', error.message);
} finally {
  console.log('This always runs');
}

// Custom Error Class
class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// Throw and catch custom errors
try {
  throw new AppError('User not found', 404);
} catch (error) {
  if (error instanceof AppError) {
    console.log(error.statusCode); // 404
  }
}

// Async error handling
async function fetchUser(id: string) {
  try {
    const user = await db.users.findUnique({ where: { id } });
    if (!user) throw new AppError('User not found', 404);
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error; // re-throw for the caller to handle
  }
}

// NestJS — Built-in exceptions
import { NotFoundException, BadRequestException } from '@nestjs/common';

@Get(':id')
async getUser(@Param('id') id: string) {
  const user = await this.userService.findOne(id);
  if (!user) throw new NotFoundException('User not found');
  return user;
}

// NestJS — Custom Exception Filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus() : 500;
    response.status(status).json({
      statusCode: status,
      message: exception instanceof Error ? exception.message : 'Internal error',
      timestamp: new Date().toISOString(),
    });
  }
}

Line-by-line walkthrough

  1. 1. Basic try/catch/finally
  2. 2. Opening try block for error handling
  3. 3. Declaring a variable
  4. 4. Catching any errors from the try block
  5. 5.
  6. 6. Finally block - runs regardless of success or failure
  7. 7. Printing output to the console
  8. 8. Closing block
  9. 9.
  10. 10. Custom Error Class
  11. 11. Declaring a class
  12. 12.
  13. 13.
  14. 14.
  15. 15.
  16. 16.
  17. 17.
  18. 18.
  19. 19. Closing block
  20. 20. Closing block
  21. 21.
  22. 22. Throw and catch custom errors
  23. 23. Opening try block for error handling
  24. 24. Throwing an error
  25. 25. Catching any errors from the try block
  26. 26. Conditional check
  27. 27. Printing output to the console
  28. 28. Closing block
  29. 29. Closing block
  30. 30.
  31. 31. Async error handling
  32. 32. Declaring a function
  33. 33. Opening try block for error handling
  34. 34. Declaring a variable
  35. 35. Conditional check
  36. 36. Returning a value
  37. 37. Catching any errors from the try block
  38. 38.
  39. 39. Throwing an error
  40. 40. Closing block
  41. 41. Closing block
  42. 42.
  43. 43. NestJS — Built-in exceptions
  44. 44. Importing required dependencies
  45. 45.
  46. 46. Decorator that adds metadata or behavior
  47. 47.
  48. 48. Declaring a variable
  49. 49. Conditional check
  50. 50. Returning a value
  51. 51. Closing block
  52. 52.
  53. 53. NestJS — Custom Exception Filter
  54. 54. Decorator that adds metadata or behavior
  55. 55. Exporting for use in other files
  56. 56. Catching any errors from the try block
  57. 57. Declaring a variable
  58. 58. Declaring a variable
  59. 59. Declaring a variable
  60. 60.
  61. 61.
  62. 62.
  63. 63.
  64. 64.
  65. 65.
  66. 66. Closing block
  67. 67. Closing block

Spot the bug

async function fetchData() {
  const res = await fetch("/api/data");
  const data = await res.json();
  return data;
}
Need a hint?
What happens if the fetch request fails?
Show answer
There is no try/catch, so if fetch fails, the error is unhandled and crashes the app. Fix: wrap in try/catch: try { ... } catch (error) { console.error('Failed:', error); }

Explain like I'm 5

Imagine doing a science experiment. Sometimes things go BOOM! Error handling is like wearing safety goggles. 'try' means 'let me try this experiment', 'catch' means 'oops, something went wrong', and 'finally' means 'clean up the lab no matter what'.

Fun fact

The most famous error in computing history is the Y2K bug. Programmers stored years as 2 digits (99 instead of 1999), meaning the year 2000 would appear as 00. Governments spent over $300 billion fixing it before midnight struck!

Hands-on challenge

Create a custom NotFoundError and ValidationError class extending Error. Write an async function that fetches a user by ID — throw NotFoundError if not found, ValidationError if the ID format is wrong. Handle both in the catch block!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook