Error Handling in NestJS: Keep it Simple, Stupid!

Summary

Sooner or later, you'll need to handle those annoying errors! So, in this post, I'll cover how to do it properly. You'll learn:

  • How to handle errors in NestJS backends
  • Hot to surface errors in a Next.js frontend

This will help you build error handling that's easy to develop and adds value to the users.

By Max Rohowsky, Ph.D.

Handling Errors in NestJS Backends

Proper error handling can save lives. Ok, it's not that dramatic, but from a user's perspective, unhandled errors are super annoying.

Ideally, if something goes wrong, the user should be able to act on a meaningful error message. And from a developer perspective, proper error handling offers helpful debugging assistance.

So, let's cover how to handle errors!

πŸ€“ Beginner: Built-in Exceptions

Out of the box, NestJS provides standard HTTP exceptions that inherit from the standard HttpException class. This includes, for example, BadRequestException, NotFoundException, ForbiddenException, etc.

The example below shows a controller that calls a service method to duplicate a post. In the service, NotFoundException is thrown if the duplication fails.

@Injectable()
export class PostsService {
  constructor(private readonly postsRepository: PostsRepository) {}

  async duplicateToDraft(postId: string, userId: string) {
    const duplicated = await this.postsRepository.duplicatePostToDraft(
      postId,
      userId,
    );
    if (!duplicated) {
      throw new NotFoundException('Failed to duplicate post');
    }
    return duplicated;
  }
}

Since we used NotFoundException with the message 'Failed to duplicate', the response looks like this:

{
  "message": "Failed to duplicate",
  "error": "Not Found",
  "statusCode": 404
}

By simply using a built-in HTTP exception, we get a nicely formatted error json.

πŸ˜‰ Intermediate: Exception Filter

If you want to control the json response shape that is sent to the client for all exceptions, you can use a global exception filter.

Use the @Catch() decorator and pass in the exception type you want to catch. The example below shows a global exception filter registered in main.ts that catches all exceptions with type HttpException.

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      message: exception.message,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

The benefit of this approach is that the json response now includes additional information like the timestamp and the request path.

{
  "message": "Failed to duplicate post",
  "statusCode": 404,
  "timestamp": "2026-01-06T16:48:22.742Z",
  "path": "/api/posts/cmk2tote800005suje3cvfiza/duplicate"
}

😎 Pro: Custom Exceptions

If the built-in exceptions don't fit your needs, you can create custom one. You probably don't need this, and the authors of the NestJS docs agree:

"In many cases, you will not need to write custom exceptions, and can use the built-in Nest HTTP exceptions."

β€” NestJS Documentation

But if you decide to make a custom exception, it should inherit from the base HttpException class:

export class CustomException extends HttpException {
  // Your custom exception
}

This ensures the built-in global filter recognizes it as an exception and sends a properly formatted HTTP response to the client.

πŸ₯·Ninja: Scoping Filters to Routes

Further above, we covered the use of useGlobalFilters to register a global filter which applies to all routes. It's also possible to scope filters to specific routes.

Here's an example of a filter scoped to a controller:

@Controller('posts')
@UseFilters(new HttpExceptionFilter())
export class PostController {
  // All route handlers in this controller use HttpExceptionFilter
}

This construction sets up the HttpExceptionFilter for every route handler defined inside the PostController.

Showing Errors on the Frontend

To this point, we've learned how to handle errors on the backend. Next, it's important to surface (some selected) errors to the user.

It only makes sense to show the errors that add value. For example, if a post fails to duplicate, it makes sense to show the error so the user understands why the duplicated post doesn't show, and that he should try the action again.

Below is a simplified example of how to show errors on the frontend. The useDuplicatePost is a React Query hook that uses a wrapper, apiFetch, around the fetch API.

// useDuplicatePost.ts
import { useMutation } from '@tanstack/react-query';
import { apiFetch } from './apiFetch';
import { toast } from 'sonner'; // or wherever your toast comes from

export function useDuplicatePost() {
  return useMutation({
    mutationFn: ({ postId, userId }: { postId: string; userId: string }) =>
      apiFetch(`/api/posts/${postId}/duplicate`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      }),
    onError: (err: any) => {
      toast.error(err.data?.message || "Something went wrong");
    },
  });
}

Here's the round-trip that ends with the error being shown to the user:

  1. User triggers the duplication (e.g., button press).
  2. The useDuplicatePost React Query hook calls apiFetch with the request.
  3. apiFetch sends the HTTP request to the NestJS backend.
  4. The backend controller and service process the request:
    • Assuming the duplication fails, the service throws NotFoundException.
  5. NestJS automatically returns a 404 JSON response.
  6. apiFetch checks res.ok:
    • It’s false because the status is 404.
  7. apiFetch reads the JSON body await res.json() and attaches it to a JS Error.
  8. apiFetch throws the structured error:
    • error.message: "Failed to duplicate post"
    • error.statusCode: 404
    • error.data: the full backend JSON
  9. React Query’s useMutation receives the error in onError.
  10. The UI displays the error using a toast (or another notification component).

Conclusion

The conclusion here is simple: 99% of your error handling needs are covered with the built-in HTTP exceptions described in the section titled "Beginner: Built-in Exceptions".

Max Rohowsky

Hey, I'm Max.

I'm an Athlete turned Finance Ph.D., Engineer, and Corporate Consultant.