PostgreSQL Error 25P02: In Failed SQL Transaction
PostgreSQL error 25P02 (in_failed_sql_transaction) occurs when you attempt to execute a SQL command inside a transaction that has already encountered an error and entered an aborted state. Once a transaction fails in PostgreSQL, it rejects all subsequent commands except ROLLBACK or ROLLBACK TO SAVEPOINT. This behavior is by design — PostgreSQL protects data integrity by refusing to execute any further statements until the failed transaction is explicitly rolled back.
Top 3 Causes
1. No Error Handling After a Failed Query
The most common cause is continuing to execute queries after one has already failed within the same transaction block, without rolling back first.
-- BAD: This triggers 25P02 on the second INSERT
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 99999); -- FK violation error
INSERT INTO payments (order_id, amount) VALUES (1, 500.00); -- ERROR 25P02!
COMMIT;
-- GOOD: Roll back and restart cleanly
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 99999); -- fails
ROLLBACK; -- clean up the aborted transaction
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 1001); -- valid product_id
INSERT INTO payments (order_id, amount) VALUES (1, 500.00);
COMMIT;
2. Constraint Violations Mid-Transaction
Violations of NOT NULL, UNIQUE, FOREIGN KEY, or CHECK constraints immediately abort the current transaction, leading to 25P02 on any follow-up queries.
-- GOOD: Use SAVEPOINT for partial rollback without losing the whole transaction
BEGIN;
INSERT INTO customers (id, name, email) VALUES (10, 'Bob', 'bob@example.com');
SAVEPOINT before_risky_insert;
INSERT INTO orders (id, customer_id, product_id)
VALUES (100, 10, 99999); -- might violate FK
-- If the above fails, roll back only to the savepoint
ROLLBACK TO SAVEPOINT before_risky_insert;
-- Retry with corrected data
INSERT INTO orders (id, customer_id, product_id)
VALUES (100, 10, 1001);
RELEASE SAVEPOINT before_risky_insert;
COMMIT;
3. Stale / Aborted Connections from Connection Pools
In pooled environments (HikariCP, PgBouncer, etc.), a connection returned to the pool after an unhandled error still holds an aborted transaction state. The next request that picks up this connection immediately gets 25P02 on its very first query.
-- Check for aborted idle transactions in your database
SELECT
pid,
usename,
application_name,
state,
xact_start,
left(query, 80) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'
ORDER BY xact_start;
-- Force-terminate stale aborted transactions (use with care in production)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'
AND xact_start < NOW() - INTERVAL '2 minutes';
Quick Fix Solutions
Step 1 — Always ROLLBACK first:
-- If you see 25P02, your first move is always:
ROLLBACK;
-- Then verify the session is clean before issuing new commands
SELECT txid_current_if_assigned() IS NULL AS no_active_transaction;
Step 2 — Use exception handling in PL/pgSQL:
CREATE OR REPLACE FUNCTION safe_order_insert(
p_order_id INT,
p_product_id INT,
p_amount NUMERIC
) RETURNS TEXT AS $$
BEGIN
INSERT INTO orders (id, product_id) VALUES (p_order_id, p_product_id);
INSERT INTO payments (order_id, amount) VALUES (p_order_id, p_amount);
RETURN 'SUCCESS';
EXCEPTION
WHEN foreign_key_violation THEN
RETURN 'ERROR: Invalid product_id ' || p_product_id;
WHEN unique_violation THEN
RETURN 'ERROR: Duplicate order_id ' || p_order_id;
WHEN OTHERS THEN
RETURN 'ERROR: ' || SQLERRM;
END;
$$ LANGUAGE plpgsql;
Prevention Tips
1. Always wrap database calls in proper try/catch blocks at the application level. Ensure every code path that can raise an exception also executes a ROLLBACK before returning the connection to the pool. Make this a non-negotiable coding standard enforced in code reviews.
2. Configure your connection pool correctly. Set a validation query (SELECT 1) and enable connection reset on return. For PgBouncer, set server_reset_query = DISCARD ALL to guarantee a clean state for every reused connection. For HikariCP, enable connectionTestQuery and set a reasonable maxLifetime to recycle connections proactively.
-- Confirm current transaction isolation and status
SHOW transaction_isolation;
-- Monitor session health regularly
SELECT pid, state, xact_start, query_start
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY xact_start NULLS LAST;
Related Errors
| Error Code | Name | Brief Description |
|---|---|---|
40001 |
serialization_failure | Often precedes 25P02 in SERIALIZABLE transactions |
40P01 |
deadlock_detected | Deadlock aborts the transaction, leading to 25P02 |
25006 |
read_only_sql_transaction | Write attempted in a read-only transaction |
23503 |
foreign_key_violation | Common root cause that triggers the aborted state |
📖 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)