What if you just wrap your entire DB layer in one giant try catch?
I know this sounds dumb but hear me out before you say anything you can’t take back. I’m using Nest.js and Prisma as the backend stack but you can read this blog and apply this for any other tech stack.
Whenever you have no idea what could go wrong, the ultimate safety net is “try catch”. One fine afternoon, I was talking to my colleague when my CEO comically said, “just wrap the entire backend in one try catch”. I thought why not.
But wait, what’s the actual problem? Why did we thought about this in the first place?
I’m kidding. It wasn’t exactly like that. There’s a particular problem with using Prisma. For those who haven’t used Prisma before, Prisma is an ORM . Think of it as something that sits between your backend and database and helps by making it easier to write database queries.
Prisma have multiple functions to get one row of a table, get multiple rows of a table, etc. The general structure goes
prisma.<model_name>.<function_name>({where:{<search_query>})
so if you have a User table and you want to find a particular user by their email, the syntax goes like
this.prisma.user.findOne({
where:{
email:"tombrady@gmail.com"
}
})
But, there’s a catch to it, or should I say try.
Every time a user can’t be found, this query will throw a Prisma Exception with a certain error code. Here’s a list of all such exceptions with their status codes (Prisma Exceptions). But what’s the problem if an exception is thrown? Well if such an exception is thrown and there’s no function to catch it, the server will die. I will explain this in details in just a minute. Let’s first see a few solutions to it.
Naive Solution
So what’s the solution? The most simplest solution will be to have your own findOne function with a try catch around it, for each database table, in their respective service and then use it everywhere. Let me rephrase this.
For your user table, you will have a user.service.ts if you are following the Nest.js pattern. In this user.service.ts, you can create a findOne implementation of your own and then in the rest of the app, you can simply use this function. The function will look like this
@Injectable()
export class UserServices {
constructor(private prisma: PrismaService) {}
async findOne(params: { where: Prisma.UserWhereUniqueInput; include: Prisma.UserInclude }): Promise<User | null> {
const { where, include } = params;
try {
return await this.prisma.user.findUnique({
where,
include,
});
} catch (err) {
console.error(err);
return null;
}
}
}
This solution works until a developer who forgot to read the internal documentation used this.prisma.user.findUnique directly. The Prisma exception raised by that will stop your service, and unless you’ve setup auto restart, the entire backend will go down.
A little safer approach
What if there was an approach with which you can sleep peacefully at night. An approach implemented in a way that you don’t have to wake up at 3AM, with your CTO next to your bed, asking to solve a P0. Introducing, global try catch. I’m not even kidding this time.
Nest.js has a concept of global exception filter. But to understand how this exception filter works, we need to understand what causes a server to stop and why is a dead server a bad thing?
What cases a server to stop?
Whenever an error is occurs anywhere in your system, the error stack trace reaches to different levels of your code. You must have seen something like
Where the error occurs in a function, but then because there is no exception handling, it bubbles up to the caller, to the caller’s caller, eventually reaching to your “main” function. In case of a backend server, if it reaches to it’s main function, the server will simply stop (Keep this stack trace in mind). Most of the frameworks have an auto restart mechanism when you run them with “prod flag”.
But in case if you are writing a server from scratch, let’s say using custom node.js and express.js you will then have to use Nodemon or Pm2 to enable auto restarts for such cases. If you don’t consider this while writing a backend service, your customers will be pretty mad about the downtime.
Now we know what not to do. So what can we do?
Do you still remember the stack trace? The problem is only if the error reaches the root. What if there’s something between your root and your buggy findOne?
If you were to create such a middleware yourself, this is a simple example of what it will look like
async function exceptionFilter(callback) {
try {
await callback();
} catch (err) {
// handle exception case
// based on different types of err, you can call different
// exception handler
}
}
function createServerInstance() {
// import all modules
// import all services
// import all controllers
// attach middlewares
// return a new instance of server class
return null;
}
function root() {
// initialize the server
const root = createServerInstance();
exceptionFilter(root.start);
}
The caller passes the all the functions which are then wrapped around by the exceptionFilter. Whenever an exception is raised and not handled by the called code, the exception filter which catch it and handle it based on exception call.
Lucky for us that Nest.js have this exception filter built in.
Goal: Setup an exception filter for Prisma related exceptions
We know that Prisma’s findOne will throw an exception that will stop our server. So let’s setup our global try/catch block.
I will show you what my finished exception filter for Prisma looks like
/**
* All Prisma related errors are not correctly handled by global error filter provided by Nestjs. Hence this filter will catch only the prisma related errors
*/
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PRISMA_CONFIGS } from "src/common/configuration/backend.config";
@Catch(
Prisma.PrismaClientInitializationError,
Prisma.PrismaClientKnownRequestError,
Prisma.PrismaClientRustPanicError,
Prisma.PrismaClientUnknownRequestError,
Prisma.PrismaClientValidationError,
)
export class AllPrismaExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
if (exception?.code === PRISMA_CONFIGS.errors.NOT_FOUND) {
httpStatus = HttpStatus.NOT_FOUND;
}
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: response.req.url,
meta: exception?.meta, // show extra information during 404 errors, so consumer knows what went wrong
};
const logBody = {
...responseBody,
metadata: {
body: {
...request.body,
},
headers: {
...request.headers,
},
},
};
// so this error is logged in our system also
console.error(logBody, exception);
response.status(httpStatus).json(responseBody);
}
}
A few good file location for this is /src/errors/prisma-exception-filter.ts or /src/middlewares/filters/prisma-exception-filter.ts.
Anyways, to attach this global filter, in your main.ts file in Nest.js root attach global filter by doing
app.useGlobalFilters(new AllPrismaExceptionFilter());
So how does this help?
Generally in your get endpoints, you will have to check with findOne if a resource exists. If it doesn’t exist, you will generally want to send a 404 with “<resource_name> not found”, error. This code will handle all such scenarios for you directly.
Not only that, in your @catch[] you can place all the exception classes that you want to be handled, and for each exception type you can write a different handler.
I’m still happy that I wrote this. It saves a lot of time. Also, in cases where if the findOne fails you need to take another action inside your service class, you can always wrap those findOne calls in try/catch and handle the catch expression directly. This filter is just an extra safeguard so you don’t end up in bed with your CTO at 3AM.