2D000ERRORTier 1 — Safe✅ HIGH confidenceinvalid 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
- 1COMMIT or ROLLBACK issued inside a PL/pgSQL function (not a procedure)
- 2A COMMIT inside a trigger function body
- 3A client-side driver issuing an autocommit COMMIT that conflicts with an open server-side transaction
- 4ROLLBACK TO SAVEPOINT issued when no matching savepoint exists
How to reproduce
A PL/pgSQL function attempts to COMMIT inside its body.
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();Fix 1: Convert the function to a procedure (Postgres 11+)
When the function genuinely needs to commit mid-execution.
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 running on Postgres 10 or earlier, or when a procedure conversion is not feasible.
-- 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
Why it's wrong: 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 docs: https://www.postgresql.org/docs/current/errcodes-appendix.html
📚 Feature docs: https://www.postgresql.org/docs/current/plpgsql-transactions.html
🔧 Source ref: src/backend/tcop/utility.c — RequireTransactionBlock()
📖 Further reading: PL/pgSQL Transaction Management
📖 Further reading: CREATE PROCEDURE
Confidence assessment
✅ HIGH confidence
Stable and well-documented. The distinction between functions and procedures for transaction control is clearly defined. The Postgres 11 procedure fix is universally recommended. Edge case: nested procedure calls each manage their own transaction sub-context; COMMIT in a nested procedure commits only the work done in that procedure's sub-transaction.
See also
🔗 Related errors
📄 Reference pages