Error Handling in NestJS: Keep it Simple, Stupid!
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.
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."
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:
- User triggers the duplication (e.g., button press).
- The
useDuplicatePostReact Query hook callsapiFetchwith the request. apiFetchsends the HTTP request to the NestJS backend.- The backend controller and service process the request:
- Assuming the duplication fails, the service throws
NotFoundException.
- Assuming the duplication fails, the service throws
- NestJS automatically returns a
404JSON response. apiFetchchecksres.ok:- Itβs
falsebecause the status is404.
- Itβs
apiFetchreads the JSON bodyawait res.json()and attaches it to a JSError.apiFetchthrows the structured error:error.message:"Failed to duplicate post"error.statusCode:404error.data: the full backend JSON
- React Queryβs
useMutationreceives the error inonError. - 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".
