Fix Prisma "Could not figure out an ID in create" panic
Find which of the two empty-insert causes you have in one psql query, apply the one-line fix, and see why Prisma 7 returns null instead of crashing.
TL;DR#
Invalid prisma.user.create() invocation: Could not figure out an ID in create is a panic in the Prisma 5/6 Rust query engine. It fires when PostgreSQL answers the INSERT ... RETURNING with zero rows while the id is an @default(autoincrement()) column the client did not send. Nothing is wrong with the schema: the asker ran prisma format, validate, migrate dev and generate without effect, and regenerating the client did not clear it in the reproduction below either. Find what swallowed the row:
- A record-and-replay test proxy. In the Stack Overflow question the app only failed under
keploy test: keploy did not match Prisma's prepared statements to the recorded mocks. Upgrade keploy (the fix is PR #1990). - A row-level BEFORE INSERT trigger that returns NULL. PostgreSQL then skips the row, and
RETURNINGhas nothing to return. Make the triggerRETURN NEW.
The error and where it came from#
The question comes from a Bun + Prisma Client 5.12.1 app on PostgreSQL, tested with cucumber.js and keploy. Normal requests worked. Under keploy test, the POST calls (signup, create-post) failed with:
Invalid `prisma.user.create()` invocation:Could not figure out an ID in create. This is a non-recoverable error which probably happens when the Prisma Query Engine has a panic.After an upgrade, the asker also got the engine's own panic line between the application logs:
thread 'tokio-runtime-worker' panicked at query-engine/connectors/sql-query-connector/src/database/operations/write.rs:194:22:
Could not figure out an ID in createThe same report was filed on the Prisma tracker as issue #23907, where a maintainer asked whether it also happened on Node instead of Bun and outside keploy. The accepted answer settles it: the problem was in how keploy matched PostgreSQL prepared statements. Keploy expected statement names in one form, Prisma's statements had another, so the replayed mocks did not line up with the queries the app sent during the test.
Why it happens: the engine reads the id from RETURNING#
The panic is the last arm of a match in the engine's create code. Here is that block from the write.rs file at the exact engine commit in the report (473ed31, the hash Prisma 5.12.1 prints), trimmed to its conditions:
match (returned_id, result_set.len(), result_set.last_insert_id()) {
// with a working RETURNING statement
(_, n, _) if n > 0 => { /* build the record from the returned row */ }
// All values provided in the write args
(Some(identifier), _, _) if !identifier.misses_autogen_value() => { /* ... */ }
// We have an auto-incremented id that we got from MySQL or SQLite
(Some(mut identifier), _, Some(num)) if identifier.misses_autogen_value() => { /* ... */ }
(_, _, _) => panic!("Could not figure out an ID in create"),
}Read it top to bottom for a PostgreSQL User with id Int @id @default(autoincrement()):
- The first arm needs at least one row back from
RETURNING. An empty result skips it. - The second arm needs the client to have sent every id value. With
autoincrement()the id is missing from the write arguments, so it does not apply. - The third arm needs a "last insert id", which, as the comment says, comes from MySQL or SQLite. PostgreSQL does not report one, so this arm does not apply either.
That leaves the panic. Prisma 5 wraps it in a PrismaClientRustPanicError and prints the "non-recoverable" text. So the message does not mean the engine is broken. It means the database round trip ended with no row and no other source for the new id.
PostgreSQL itself can produce that empty result. Its trigger behaviour documentation says a row-level trigger fired before an operation "can return NULL to skip the operation for the current row." A skipped row is not inserted, so RETURNING returns nothing and the command tag is INSERT 0 0. A replay proxy that answers with a mock recorded for a different statement gives the engine a reply that does not belong to its INSERT. The accepted answer traces the keploy failure to that mismatch; the exact reply keploy sent is not reproduced here.
The panic, reproduced#
The engine side was reproduced on 19 October 2026 with Prisma 5.12.1 (engine 473ed31, the same hash as the report), Node.js 20.20.2 and the official postgres:16-alpine image (PostgreSQL 16.15), using the User model from the question. No Bun and no keploy: one script calls prisma.user.create().
A baseline insert works:
CREATED {"id":1,"email":"ada@example.co.uk","name":"Ada","password":"x","createdAt":"2026-10-19T02:30:17.898Z","updatedAt":"2026-10-19T02:30:17.898Z"}Then a single change: a BEFORE INSERT row trigger whose function only does RETURN NULL. The next create() fails with the exact panic from the question, same file, same line:
thread 'tokio-runtime-worker' panicked at query-engine/connectors/sql-query-connector/src/database/operations/write.rs:194:22:
Could not figure out an ID in create
ERROR CLASS: PrismaClientRustPanicErrorNo row was written, and the same statement typed into psql shows why the engine had nothing to read:
id
----
(0 rows)
INSERT 0 0Running npx prisma generate again with the trigger unchanged gave the same panic, which matches the asker's experience: regenerating the client cannot fix a database that returns no row. Changing the trigger function to RETURN NEW, and nothing else, made the same call succeed:
CREATED {"id":5,"email":"grace@example.co.uk","name":"Ada","password":"x","createdAt":"2026-10-19T02:30:36.392Z","updatedAt":"2026-10-19T02:30:36.392Z"}Note the id: 5, not 2. Each skipped insert still consumed a value from the User_id_seq sequence (the default runs before the trigger), so failed creates leave gaps in the ids.
Fix#
1. Run the failing insert outside the test harness#
If create() only fails under a test runner or a proxy, and the same request against the real database works, go to step 2. If it also fails against the real database, look for a trigger on the table (PostgreSQL folds unquoted names to lowercase, so keep the double quotes, the same trap as in Prisma's 42P01 table does not exist error):
SELECT tgname, tgenabled, pg_get_triggerdef(oid)
FROM pg_trigger
WHERE tgrelid = '"User"'::regclass
AND NOT tgisinternal;Any BEFORE INSERT ... FOR EACH ROW trigger in that list is a suspect. Then go to step 3.
2. Keploy: upgrade to a release with the prepared-statement fix#
Keploy fixed the matching in PR #1990, "added check for prisma prepare statement matching", merged on 25 June 2024. The diff changes keploy's PostgreSQL integration (decode.go and match.go) so that statement names of Prisma's form are recognised next to the ones it already handled. The merge commit is exactly the v2.2.0-alpha14 tag, so any keploy from v2.2.0-alpha14 on has it. The keploy issue that tracked the bug, keploy/keploy#1944, was opened with keploy 2.1.0-alpha23 and closed on 4 July 2024 after the reporter confirmed the latest keploy resolved it. The accepted answer reports the passing run on alpha-17.
Install a current keploy with the official installation instructions, then run the same test command as before:
keploy test -c "bun --watch index.ts"3. Trigger: return the row#
A BEFORE trigger that is only meant to check or adjust the row must hand the row back. The same PostgreSQL page says a BEFORE trigger that does not mean to skip or change the row "must be careful to return as its result the same row that was passed in", which for an INSERT is NEW. The minimal change is in the trigger function, not in Prisma:
CREATE OR REPLACE FUNCTION skip_insert() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- checks or changes to NEW go here
RETURN NEW;
END $$;If the trigger really is meant to stop some rows, the create() call will keep panicking for exactly those rows, because a skipped insert has no id to return. Raise an error in the trigger instead (RAISE EXCEPTION), so the insert fails with a database error rather than a silent empty result.
Verify the fix#
Run the same create() call and the same count from psql. A fixed trigger gives a CREATED line with a new id, and the row is in the table. For keploy, the POST test cases that used to fail on the panic should now get past the database call. In the accepted answer, the first test case passed after the upgrade, and the next one failed on a different token value in the login response. Decoding the two JWT payloads shown in the diff gives the same user and email but different iat and exp timestamps, about five minutes apart: a fresh token minted at replay time. That is keploy noise, not Prisma. The keploy configuration file reference lists globalNoise for "Noisy fields to be ignored at global/test-set level", and its example says "To ignore the entire field, pass an empty array".
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
A trigger that moves the row elsewhere still panics#
Some schemas use a BEFORE trigger to send the row to another table (an archive, or old trigger-based partitioning) and then return NULL so the parent table stays empty. The data is saved, yet Prisma still panics. Reproduced with a trigger that copies NEW.* into a UserArchive table before returning NULL:
thread 'tokio-runtime-worker' panicked at query-engine/connectors/sql-query-connector/src/database/operations/write.rs:194:22:
Could not figure out an ID in create in_user | in_archive
---------+------------
0 | 1The symptom matches prisma/orm#24566, where the reporter saw the error although "the data has already been added to the database", and later put it down to a setting of their database without saying which. Retrying that create() writes a second copy. Either point the Prisma model at the table that actually keeps the row, or do the copy in an AFTER INSERT trigger: PostgreSQL documents that "The return value is ignored for row-level triggers fired after an operation", so the original insert and its RETURNING row survive.
Not Bun, and not always fatal for the client#
Two details from the reproduction answer questions raised in the Prisma issue. First, the run used Node.js, not Bun, and produced the same panic at the same line, so this panic is not specific to Bun. The run says nothing about how Bun and keploy behave together, which the keploy fix covers. Second, despite "non-recoverable", the same Prisma Client instance answered the next query in the same process after the panic:
SECOND QUERY (same client) count = 1Prisma 7: no panic, create() returns null#
The panic string is still in the Rust SQL connector of the last Prisma 6 engines (release 6.19.0). In the Prisma 7 engines (release 7.0.0), the connectors folder no longer has a sql-query-connector, because Prisma ORM 7 builds queries with its query compiler and runs them through a driver adapter. The same trigger was tested with Prisma 7.10.1, the prisma-client generator and @prisma/adapter-pg:
$ npx tsx create.ts barbara@example.co.uk (trigger returns NULL)
CREATED nullNo error, no row, and a create() result of null even though its TypeScript type is the model. After an upgrade to Prisma 7, the crash turns into a TypeError wherever the code first reads user.id. If you are upgrading to Prisma 7 anyway, the Prisma v7 upgrade page on paths[1] covers the adapter setup Prisma 7 requires. A guard keeps the failure loud:
const user = await prisma.user.create({ data: { email, name, password } })
if (!user) throw new Error('INSERT returned no row: check BEFORE INSERT triggers on "User"')Prepared statements going through something other than PostgreSQL itself cause another classic Prisma failure. If your error is prepared statement "s0" already exists behind a connection pooler, see the s0 prepared statement fix. For a create() that fails on the database server instead, with a nested connect and syntax error at or near "ON", the cause is the server version: see the nested connect error on old PostgreSQL.
In short, "Could not figure out an ID in create" is the engine telling you the database sent back nothing it could use as the new row. Whether that nothing came from a test proxy replaying the wrong answer or from a trigger that quietly dropped the row, the fix is upstream of Prisma: upgrade the proxy, or make the trigger return NEW. Then the same create() call works without touching the schema or regenerating the client.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Prisma 'Unique constraint failed on the fields: (id)'
Prove in two queries that the id sequence is stuck behind your imported rows, reset it safely even on an empty table, and see why retrying seems to fix it.
Fix: Can't Migrate Schema Using Prisma With Supabase
The message comes from PgBouncer, and the same mistake now shows up as a silent hang: which port Prisma Migrate needs on Supabase, and how to keep both URLs in Prisma 6 and 7.
Prisma Migrate P3014: permission denied to create database
Hit `P3014: permission denied to create database` when running Prisma Migrate on Heroku Postgres? Use prisma db push, a separate shadow database, or grant CREATEDB — and fix hangs with Supabase pooled connections.


