How to NestJS Catch Global Prisma Error in Your Application
- backlinksindiit
- 6 days ago
- 5 min read
Your API returns 500 Internal Server Error for everything. Unique constraint violations? 500. Record not found? 500. Foreign key failures? Still 500. Users see nothing helpful, developers get zero context, and debugging production issues becomes guesswork.
This happens because Prisma throws PrismaClientKnownRequestError exceptions that NestJS doesn't handle by default. The built-in exception layer catches them, sure, but treats every database error identically. We need proper NestJS catch global Prisma error handling that maps each Prisma error code to appropriate HTTP status codes.
Project Structure and File Organization
Before writing code, understand where files go. Standard NestJS project with Prisma looks like this:
src/
├── app.module.ts
├── main.ts
├── filters/
│ └── prisma-client-exception.filter.ts
└── prisma/
└── prisma.service.ts
The filter lives in src/filters/. Some teams put it in src/common/filters/, others in src/shared/. Pick one structure, stick with it across projects. Consistency beats clever organization.
Building the Exception Filter Step-by-Step
Create src/filters/prisma-client-exception.filter.ts:
import { ArgumentsHost, Catch, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Prisma } from '@prisma/client';
import { Response } from 'express';
@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter extends BaseExceptionFilter {
catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
console.error('Prisma Error:', exception.code, exception.message);
switch (exception.code) {
case 'P2002': {
const status = HttpStatus.CONFLICT;
response.status(status).json({
statusCode: status,
message: 'A record with this value already exists',
error: 'Conflict',
});
break;
}
case 'P2025': {
const status = HttpStatus.NOT_FOUND;
response.status(status).json({
statusCode: status,
message: 'Record not found',
error: 'Not Found',
});
break;
}
case 'P2003': {
const status = HttpStatus.BAD_REQUEST;
response.status(status).json({
statusCode: status,
message: 'Foreign key constraint failed',
error: 'Bad Request',
});
break;
}
case 'P2014': {
const status = HttpStatus.BAD_REQUEST;
response.status(status).json({
statusCode: status,
message: 'Required relation violation',
error: 'Bad Request',
});
break;
}
default:
super.catch(exception, host);
break;
}
}
}
That console.error logs errors during development. Production apps should pipe these to monitoring tools like Sentry or DataDog instead. The switch statement handles the four most common Prisma errors covering 90% of production cases.
Applying the Filter Globally
Update src/main.ts to register the filter:
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaClientExceptionFilter } from './filters/prisma-client-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
The HttpAdapterHost provides access to the underlying HTTP framework. Miss this and the filter breaks. Projects using Fastify instead of Express need the same setup - the adapter handles framework differences automatically.
Testing Your Error Handling Works
Write integration tests verifying correct status codes. Create test/error-handling.e2e-spec.ts:
import { Test } from '@nestjs/testing'; import { INestApplication, HttpStatus } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; describe('Error Handling (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports:
,
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('returns 409 for unique constraint violations', async () => {
await request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(HttpStatus.CREATED);
return request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Duplicate' })
.expect(HttpStatus.CONFLICT);
});
it('returns 404 for missing records', async () => {
return request(app.getHttpServer())
.patch('/users/99999')
.send({ name: 'Updated' })
.expect(HttpStatus.NOT_FOUND);
});
afterAll(async () => {
await app.close();
});
});
Tests catch regressions. Someone refactors error handling, breaks status codes, tests fail immediately. Without tests, broken error handling reaches production where users discover it first.
Complete Prisma Error Code Reference
Building comprehensive error handling means covering edge cases too. What about connection timeouts (P2024)? Transaction failures (P2034)? Database accessibility issues (P1001)? Production apps handle these explicitly.
Environment-Specific Error Messages
Development needs detailed errors. Production needs sanitized messages preventing information leakage. Implement environment checks:
catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
console.error('Prisma Error Details:', {
code: exception.code,
meta: exception.meta,
message: exception.message,
});
}
const message = isDevelopment
? exception.message
: 'A database error occurred';
// Handle specific error codes...
}
Development logs include exception.meta showing which fields caused violations. Production hides this, returning generic messages that keep database schema details private.
The Shortcut: Using nestjs-prisma Package
Building filters manually teaches fundamentals. Production projects benefit from battle-tested packages. The nestjs-prisma library handles error mapping automatically:
npm install nestjs-prisma
Import and configure in app.module.ts:
import { Module } from '@nestjs/common'; import { PrismaModule } from 'nestjs-prisma'; @Module({ imports: [ PrismaModule.forRoot({ isGlobal: true, prismaServiceOptions: { prismaOptions: { log: <'error', 'warn'>
,
},
explicitConnect: true,
},
}),
],
})
export class AppModule {}
Then apply the pre-built filter in main.ts:
import { PrismaClientExceptionFilter } from 'nestjs-prisma';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter));
await app.listen(3000);
}
The package maintains error code mappings, handles edge cases, supports both REST and GraphQL, and receives updates when Prisma adds new error codes. Trade-off is adding a dependency. Teams comfortable maintaining custom filters save the package size.
Troubleshooting When Filters Stop Working
Filter not catching errors? Check three things:
Filter registered globally in main.ts? Method-scoped or controller-scoped filters won't catch errors from other parts of the application.
Passing HttpAdapterHost to filter constructor? Without it, BaseExceptionFilter cannot format responses correctly.
Using await on Prisma queries? Unwrapped promises never throw catchable exceptions.
Still getting 500 errors? Add logging inside the filter's catch method. If logs appear, the filter works but status code mapping failed. If logs never appear, the filter isn't being invoked - registration problem.
Different error types need different filters. Validation errors from class-validator require separate handling. Authentication failures need their own filter. Database connection issues during startup throw PrismaClientInitializationError, not PrismaClientKnownRequestError. Build separate filters for distinct error categories.
Before and After Comparison
Without proper error handling:
{
"statusCode": 500,
"message": "Internal server error"
}
With NestJS catch global Prisma error handling:
{
"statusCode": 409,
"message": "A record with this value already exists",
"error": "Conflict"
}
The difference transforms user experience. Signup fails with clear feedback instead of generic errors. Support teams spend less time debugging because logs contain actual error codes. Monitoring dashboards separate user errors (400s) from system failures (500s).
Applications serving clients developed by teams working with mobile app development houston benefit from proper status codes. Mobile apps handle 409 conflicts differently than 500 server errors, showing appropriate retry logic or user feedback messages.
Implementation Checklist:
Create PrismaClientExceptionFilter in src/filters/ directory
Import required dependencies: @nestjs/common, @nestjs/core, @prisma/client
Extend BaseExceptionFilter for automatic fallback handling
Map P2002, P2025, P2003, P2014 to appropriate HTTP status codes
Register filter globally in main.ts using HttpAdapterHost
Add environment checks for development vs production error messages
Write integration tests verifying correct status codes returned
Log errors to monitoring systems in production environments
Consider nestjs-prisma package for maintained error mappings
Handle connection errors separately from query errors
Document which error codes your application handles explicitly
Review error handling during code reviews and security audits
NestJS catch global Prisma error implementations separate good APIs from great ones. Users get helpful feedback, developers get useful logs, monitoring systems track meaningful metrics. The initial setup takes an hour. The time saved debugging production issues pays that back within days.
Comments