2D000
PostgreSQLERRORCommonInvalid Transaction TerminationHIGH confidence

invalid transaction termination

What this means

A COMMIT or ROLLBACK command was issued in a context where transaction termination is not permitted, such as inside a PL/pgSQL function body or a trigger. Transaction control within functions is restricted to procedures called with CALL.

Why it happens
  1. 1COMMIT or ROLLBACK issued inside a PL/pgSQL function (not a procedure)
  2. 2A COMMIT inside a trigger function body
  3. 3A client-side driver issuing an autocommit COMMIT that conflicts with an open server-side transaction
  4. 4ROLLBACK TO SAVEPOINT issued when no matching savepoint exists
How to reproduce

A PL/pgSQL function attempts to COMMIT inside its body.

trigger — this will error
trigger — this will error
CREATE OR REPLACE FUNCTION bad_commit() RETURNS void AS $
BEGIN
  INSERT INTO log_table (msg) VALUES ('hello');
  COMMIT; -- triggers 2D000
END;
$ LANGUAGE plpgsql;

SELECT bad_commit();

expected output

ERROR:  invalid transaction termination

Fix 1

Convert the function to a procedure (Postgres 11+)

WHEN When the function genuinely needs to commit mid-execution.

Convert the function to a procedure (Postgres 11+)
CREATE OR REPLACE PROCEDURE transactional_work()
LANGUAGE plpgsql AS $
BEGIN
  INSERT INTO log_table (msg) VALUES ('step 1');
  COMMIT;
  INSERT INTO log_table (msg) VALUES ('step 2');
  COMMIT;
END;
$;

CALL transactional_work();

Why this works

In Postgres 11+, stored procedures (created with CREATE PROCEDURE and called with CALL) support transaction control (COMMIT and ROLLBACK) within their body. Functions called with SELECT do not, because functions participate in the calling query's transaction. Procedures have their own transaction management context.

Fix 2

Restructure to avoid mid-function commits

WHEN When running on Postgres 10 or earlier, or when a procedure conversion is not feasible.

Restructure to avoid mid-function commits
-- Move all work into a single transaction managed by the caller:
BEGIN;
INSERT INTO log_table (msg) VALUES ('step 1');
INSERT INTO log_table (msg) VALUES ('step 2');
COMMIT;

-- Use SAVEPOINT for partial rollback within a transaction:
BEGIN;
SAVEPOINT sp1;
INSERT INTO log_table (msg) VALUES ('step 1');
SAVEPOINT sp2;
INSERT INTO log_table (msg) VALUES ('step 2');
-- If step 2 fails: ROLLBACK TO SAVEPOINT sp2;
COMMIT;

Why this works

Transaction control belongs to the client layer. By managing COMMIT and ROLLBACK at the caller level and using SAVEPOINTs for partial rollback, the function body performs only data operations without needing transaction control commands, which are disallowed in function context.

What not to do

Use EXECUTE to dynamically run COMMIT inside a function

Dynamic EXECUTE of COMMIT still runs inside the function's transaction context and raises the same 2D000 error.

Version notes
Postgres 11+

Stored procedures with CALL support transaction control (COMMIT/ROLLBACK). This was not available in functions in any version.

Sources
Official documentation ↗

src/backend/tcop/utility.c — RequireTransactionBlock()

PL/pgSQL Transaction ManagementCREATE PROCEDURE

Content generated with AI assistance and reviewed for accuracy. Found an error? hello@errcodes.dev

← All PostgreSQL errors