PostgreSQL Error 25P02: In Failed SQL Transaction
PostgreSQL error 25P02 (in_failed_sql_transaction) occurs when you attempt to execute SQL commands inside a transaction that has already been marked as aborted due to a prior error. Once any statement within a BEGIN block fails, PostgreSQL immediately flags the entire transaction as aborted and rejects all subsequent commands — except ROLLBACK. The only way to recover is to issue a ROLLBACK and start a fresh transaction.
Top 3 Causes
1. Ignoring Errors Inside a Transaction Block
The most common cause is simply continuing to run queries after one has failed within the same transaction. Many application drivers won't automatically roll back for you.
-- This triggers 25P02 on the SELECT
BEGIN;
INSERT INTO products (id, name) VALUES (1, 'Widget');
INSERT INTO products (id, name) VALUES (1, 'Gadget'); -- ERROR: duplicate key
SELECT * FROM products; -- ERROR 25P02: in failed sql transaction
COMMIT;
-- Correct approach: rollback on error, then retry
BEGIN;
INSERT INTO products (id, name) VALUES (1, 'Widget');
INSERT INTO products (id, name) VALUES (1, 'Gadget'); -- ERROR
ROLLBACK;
BEGIN;
INSERT INTO products (id, name) VALUES (2, 'Gadget'); -- retry with new key
COMMIT;
2. Missing SAVEPOINT for Partial Error Recovery
When you need to handle expected errors without aborting the whole transaction, use SAVEPOINT. Without it, a single failure kills the entire transaction context.
BEGIN;
INSERT INTO accounts (id, balance) VALUES (10, 1000.00);
SAVEPOINT before_transfer;
UPDATE accounts SET balance = balance - 500 WHERE id = 99; -- row may not exist
-- If this causes an error, rollback only to savepoint
ROLLBACK TO SAVEPOINT before_transfer;
-- Transaction is still alive — continue safely
UPDATE accounts SET balance = balance + 500 WHERE id = 10;
COMMIT;
3. Unhandled Exceptions in PL/pgSQL Functions
If a stored function raises an unhandled exception, it aborts the caller's transaction, causing every subsequent statement in that transaction to return 25P02.
-- Dangerous: no exception handling
CREATE OR REPLACE FUNCTION risky_insert(p_id INT)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id) VALUES (p_id); -- if this fails, caller's txn is aborted
END;
$$ LANGUAGE plpgsql;
-- Safe: wrap with EXCEPTION block
CREATE OR REPLACE FUNCTION safe_insert(p_id INT)
RETURNS TEXT AS $$
BEGIN
INSERT INTO orders (id) VALUES (p_id);
RETURN 'OK';
EXCEPTION
WHEN unique_violation THEN
RETURN 'DUPLICATE';
WHEN OTHERS THEN
RAISE NOTICE 'Unexpected error: %', SQLERRM;
RETURN 'ERROR';
END;
$$ LANGUAGE plpgsql;
Quick Fix Solutions
- Always ROLLBACK on error — never reuse a connection that's in an aborted transaction state.
- Use SAVEPOINT for granular error recovery without losing the entire transaction.
- Add EXCEPTION blocks in all PL/pgSQL functions that perform DML.
-
Monitor aborted sessions using
pg_stat_activity:
SELECT pid, usename, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)';
-- Terminate stuck aborted sessions if needed
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'
AND state_change < NOW() - INTERVAL '5 minutes';
Prevention Tips
Set idle_in_transaction_session_timeout to auto-terminate sessions stuck in a failed transaction state:
-- Apply globally in postgresql.conf or per role
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';
Implement proper error handling at the application layer — always wrap DB calls in try/catch blocks, rollback on any exception, and validate connection health before returning it to the pool. Libraries like HikariCP or PgBouncer can help reset connection state automatically between requests.
Related Errors
| Code | Name | Relation |
|---|---|---|
| 40P01 | deadlock_detected | Causes transaction abort → leads to 25P02 |
| 23505 | unique_violation | Common trigger that aborts a transaction |
| 40001 | serialization_failure | Requires retry logic; leaves txn in aborted state |
| 25001 | active_sql_transaction | Cannot start a new txn inside an existing one |
📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.
Top comments (0)