DEV Community

Cover image for Why My Prisma P2002 Was Escaping try/catch in NestJS
Rhico
Rhico

Posted on

Why My Prisma P2002 Was Escaping try/catch in NestJS

I'm building RunHop in public, a social + event platform for running races. Today I worked on the Reactions module: likes on posts.

The module itself was straightforward:

  • POST /posts/:id/likes
  • DELETE /posts/:id/likes
  • a ReactionService that talks to Prisma's postLike model
  • unit tests and e2e coverage for the happy path and duplicate-like path

The interesting bug was in the duplicate-like flow.

The Service Looked Correct

I had this shape in src/domain/social/reaction/reaction.service.ts:

async like(postId: string, userId: string) {
    const post = await this.postService.findById(postId);

    if (!post) throw new NotFoundException('Post not found');

    try {
        return this.prisma.postLike.create({
            data: { postId, userId },
        });
    } catch (error) {
        if (
            error instanceof PrismaClientKnownRequestError &&
            error.code === 'P2002'
        ) {
            throw new ConflictException('Duplicate like');
        }

        throw error;
    }
}
Enter fullscreen mode Exit fullscreen mode

At a glance it feels fine. There's a try/catch, Prisma throws P2002 for a unique constraint violation, and I map it to ConflictException.

But the unit test still failed.

Why It Failed
postLike.create() returns a promise. When that promise rejects, the rejection happens asynchronously.

Because I returned the promise directly from inside the try block, the function exited before the rejection occurred. The catch block never saw the Prisma error.

So instead of this:

Prisma rejects with P2002
service catches it
Nest gets ConflictException

I got this:

Prisma rejects with P2002
promise escapes
raw Prisma error bubbles up
The Fix
The fix was to await inside the try block:

try {
    return await this.prisma.postLike.create({
        data: { postId, userId },
    });
} catch (error) {
    if (
        error instanceof PrismaClientKnownRequestError &&
        error.code === 'P2002'
    ) {
        throw new ConflictException('Duplicate like');
    }

    throw error;
}
Enter fullscreen mode Exit fullscreen mode

This is one of the few cases where return await is exactly what you want. It keeps the rejected promise inside the try/catch boundary.

The Rest of the Module

I also added:

ownershipCheck(likeId, userId) so only the owner can delete a like unlike() mapping Prisma P2025 to NotFoundException
an e2e test that does the full flow:
register
create post
like
duplicate like -> 409
unlike
unlike again -> 404

One design detail I still want to revisit: the delete route is DELETE /posts/:id/likes, but the :id there is currently a like id, not a post id. It works, but the route shape is carrying two meanings between create and delete.

Takeaway

The useful reminder from this session:

try/catch only catches what stays inside it.

If you need to translate async database errors into framework exceptions, you need the rejected promise to be awaited inside the try block.

Top comments (0)