From ac061482f54aeebbd9ad1449ef0049fdba7bdb30 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 17:52:16 +0000 Subject: [PATCH] fix(prisma): settle a recorded query without leaking its rejection The Prisma hook records each model query as a function call and marks it returned when the query's promise settles. It did that with `void result.finally(...)`. finally() returns a new promise that rejects whenever the query rejects, and nothing awaited it, so a query that failed and was caught by the application still surfaced as an unhandled rejection. Under vitest that ends the run with exit code 1 and no recording is written. A caught P2002 unique violation in a sign up path is what showed it. Attach the settle callback with then(settle, settle) instead. functionReturn already attaches its own handlers to the query's promise and rewrites the return event as an exception event when the promise rejects, so the recorded shape of a failed query is unchanged. Only the stray rejection goes away. The Prisma fixture now includes a create that fails on the unique email and is caught. Before this change the fixture exits 1 on that statement. The snapshot gains the failed create, recorded as an exception, and its SQL. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EgK3BVLCovf26kotqTnkau --- src/hooks/prisma.ts | 12 ++- test/__snapshots__/prisma.test.ts.snap | 103 +++++++++++++++++++++---- test/prisma/script.js | 13 ++++ 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/src/hooks/prisma.ts b/src/hooks/prisma.ts index bf3e1c1..18c103c 100644 --- a/src/hooks/prisma.ts +++ b/src/hooks/prisma.ts @@ -171,12 +171,20 @@ function createPrismaClientMethodProxy unknown const result = target.apply(thisArg, argArray); assert(isPromise(result)); - void result.finally(() => { + // Settle the recorded call whether the query resolves or rejects. + // result.finally(cb) returns a new promise that rejects along with a + // failed query, and nothing awaited it, so a rejected query the + // application caught (a P2002 unique violation, for example) still + // surfaced as an unhandled rejection. functionReturn attaches its own + // handlers to the promise and turns the return event into an exception + // event when the query rejects. + const settle = () => { queryMethodContext = undefined; recordings.forEach((recording, idx) => recording.functionReturn(calls[idx].id, result, startTime), ); - }); + }; + result.then(settle, settle); return result; } catch (exn: unknown) { diff --git a/test/__snapshots__/prisma.test.ts.snap b/test/__snapshots__/prisma.test.ts.snap index 6ba2699..4dcc9b6 100644 --- a/test/__snapshots__/prisma.test.ts.snap +++ b/test/__snapshots__/prisma.test.ts.snap @@ -470,13 +470,90 @@ exports[`mapping Prisma tests 1`] = ` "defined_class": "User", "event": "call", "id": 35, + "lineno": 4, + "method_id": "create", + "parameters": [ + { + "class": "Object", + "name": "args", + "object_id": 10, + "properties": [ + { + "class": "Object", + "name": "data", + "properties": [ + { + "class": "String", + "name": "name", + }, + { + "class": "String", + "name": "email", + }, + ], + }, + ], + "value": "{ data: { name: 'Alice again', email: 'alice@prisma.io' } }", + }, + ], + "path": "@prisma/client/User", + "receiver": { + "class": "String", + "value": "'User'", + }, + "static": false, + "thread_id": 0, + }, + { + "event": "call", + "id": 36, + "sql_query": { + "database_type": "sqlite", + "sql": "INSERT INTO \`main\`.\`User\` (\`email\`, \`name\`) VALUES (?,?) RETURNING \`id\` AS \`id\`, \`email\` AS \`email\`, \`name\` AS \`name\`", + }, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "id": 37, + "parent_id": 36, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "exceptions": [ + { + "class": "PrismaClientKnownRequestError", + "message": " +Invalid \`prisma.user.create()\` invocation: + + +Unique constraint failed on the fields: (\`email\`)", + "object_id": 12, + }, + ], + "id": 38, + "parent_id": 35, + "return_value": { + "class": "Promise", + "object_id": 11, + "value": "Promise { }", + }, + "thread_id": 0, + }, + { + "defined_class": "User", + "event": "call", + "id": 39, "lineno": 1, "method_id": "findMany", "parameters": [ { "class": "Object", "name": "args", - "object_id": 10, + "object_id": 13, "properties": [ { "class": "Object", @@ -518,7 +595,7 @@ exports[`mapping Prisma tests 1`] = ` }, { "event": "call", - "id": 36, + "id": 40, "sql_query": { "database_type": "sqlite", "sql": "SELECT \`main\`.\`User\`.\`id\`, \`main\`.\`User\`.\`email\`, \`main\`.\`User\`.\`name\` FROM \`main\`.\`User\` WHERE \`main\`.\`User\`.\`name\` LIKE ? LIMIT ? OFFSET ?", @@ -528,13 +605,13 @@ exports[`mapping Prisma tests 1`] = ` { "elapsed": 31.337, "event": "return", - "id": 37, - "parent_id": 36, + "id": 41, + "parent_id": 40, "thread_id": 0, }, { "event": "call", - "id": 38, + "id": 42, "sql_query": { "database_type": "sqlite", "sql": "SELECT \`main\`.\`Post\`.\`id\`, \`main\`.\`Post\`.\`title\`, \`main\`.\`Post\`.\`content\`, \`main\`.\`Post\`.\`published\`, \`main\`.\`Post\`.\`authorId\` FROM \`main\`.\`Post\` WHERE \`main\`.\`Post\`.\`authorId\` IN (?) LIMIT ? OFFSET ?", @@ -544,18 +621,18 @@ exports[`mapping Prisma tests 1`] = ` { "elapsed": 31.337, "event": "return", - "id": 39, - "parent_id": 38, + "id": 43, + "parent_id": 42, "thread_id": 0, }, { "elapsed": 31.337, "event": "return", - "id": 40, - "parent_id": 35, + "id": 44, + "parent_id": 39, "return_value": { "class": "Promise", - "object_id": 11, + "object_id": 14, "value": "Promise { [ { id: 2, email: 'bob@prisma.io', name: 'Bob', posts: [Array] } ] }", }, "thread_id": 0, @@ -606,7 +683,7 @@ exports[`mapping Prisma tests 1`] = ` }, { "event": "call", - "id": 41, + "id": 45, "sql_query": { "database_type": "sqlite", "sql": "SELECT 1", @@ -616,8 +693,8 @@ exports[`mapping Prisma tests 1`] = ` { "elapsed": 31.337, "event": "return", - "id": 42, - "parent_id": 41, + "id": 46, + "parent_id": 45, "thread_id": 0, }, ], diff --git a/test/prisma/script.js b/test/prisma/script.js index 55f2190..d14471f 100644 --- a/test/prisma/script.js +++ b/test/prisma/script.js @@ -46,6 +46,19 @@ async function main() { console.log(bob); + // A query the application catches must not escape as an unhandled rejection. + // The email is unique, so this create fails with P2002. + try { + await prisma.user.create({ + data: { + name: "Alice again", + email: "alice@prisma.io", + }, + }); + } catch (error) { + console.log("caught:", error.code); + } + const bobsWithPosts = await prisma.user.findMany({ include: { posts: true,