PostgreSQL error 25P01 occurs when attempting to commit, rollback, or use SAVEPOINT outside an active transaction block. This commonly happens when a transaction is prematurely terminated, the connection drops, or your application's transaction tracking is out of sync with the database. The fix involves ensuring transactions are properly started before attempting to end them.
PostgreSQL error 25P01 "No active SQL transaction" (SQLSTATE 25P01) indicates that an operation was attempted that requires an active transaction, but no transaction is currently running in the session. This error occurs at the PostgreSQL server level when: 1. **Attempting to COMMIT or ROLLBACK without a transaction**: Your application code calls COMMIT or ROLLBACK, but there is no active transaction to commit or roll back. 2. **Using SAVEPOINT outside a transaction**: PostgreSQL SAVEPOINTs are subtransactions that can only exist within a larger transaction block. Attempting to create a SAVEPOINT when no outer transaction is active will trigger this error. 3. **Nested transaction mismatch**: Your application maintains its own transaction counter or stack, but it becomes out of sync with the actual database transaction state. This can happen when connections drop unexpectedly or exceptions are caught but transaction tracking is not updated. 4. **Connection timeout or reset**: A long-running transaction times out or the connection is reset by the server, but the client tries to commit/rollback anyway. The error is particularly common in ORMs like Sequelize, TypeORM, or in frameworks like Laravel that manage nested transactions through savepoints.
Ensure your application always wraps operations in a transaction before attempting to commit or rollback:
Correct pattern in psql:
BEGIN; -- Start the transaction
SELECT * FROM users WHERE id = 1;
UPDATE users SET name = 'Alice' WHERE id = 1;
COMMIT; -- End the transactionWithout a transaction (will fail):
-- NO BEGIN STATEMENT
SELECT * FROM users;
COMMIT; -- ERROR: 25P01 - No active SQL transactionCheck your application code to ensure BEGIN (or START TRANSACTION) is called before any database operations within a transaction block.
If using nested transactions with SAVEPOINT, ensure the outer transaction is active:
Correct pattern:
BEGIN;
-- Outer transaction is active
SAVEPOINT sp1;
UPDATE table1 SET col1 = 'value';
ROLLBACK TO sp1;
COMMIT; -- This works because the outer transaction is still activeIncorrect pattern:
SAVEPOINT sp1; -- ERROR: 25P01 - No outer transaction
UPDATE table1 SET col1 = 'value';If using an ORM like Sequelize or TypeORM, use their transaction methods:
// Sequelize
await sequelize.transaction(async (transaction) => {
await User.update({ name: 'Alice' }, { where: { id: 1 }, transaction });
// COMMIT is automatic when the callback completes
});When an error occurs within a transaction, PostgreSQL enters an "aborted transaction" state (SQLSTATE 25P02). Attempting to run more statements will fail.
Correct error handling in psql:
BEGIN;
UPDATE users SET id = 1 WHERE id = 999; -- Might fail
-- If an error occurred, the transaction is now ABORTED
-- You MUST ROLLBACK before starting a new transaction
ROLLBACK; -- Always roll back on error
-- Now you can start a new transaction
BEGIN;
-- Try again or take a different action
COMMIT;In Node.js with pg library:
try {
const client = await pool.connect();
await client.query('BEGIN');
await client.query('UPDATE users SET name = $1 WHERE id = $2', ['Alice', 1]);
await client.query('COMMIT');
client.release();
} catch (error) {
console.error('Transaction failed:', error.message);
// ROLLBACK is implicit when the connection is released on error
// Or explicitly:
// await client.query('ROLLBACK');
}If using Laravel, TypeORM, or Sequelize, recent versions may have fixed transaction handling issues. A known issue occurred in Laravel 10.25.2 with nested transactions.
For Laravel:
# Update to the latest version
composer update laravel/frameworkCheck the framework's transaction documentation to ensure you're using the correct syntax for nested transactions:
DB::transaction(function () {
// Outer transaction
DB::transaction(function () {
// Inner transaction (savepoint in PostgreSQL)
User::create(['name' => 'Alice']);
});
});For Sequelize:
Ensure you're using the transaction helper correctly:
await sequelize.transaction(async (t) => {
await User.create({ name: 'Alice' }, { transaction: t });
});If autocommit is enabled, PostgreSQL executes each statement immediately without a transaction. This prevents explicit COMMIT/ROLLBACK.
Check autocommit status in psql:
-- PostgreSQL has autocommit ON by default
-- (each statement auto-commits unless in a transaction block)
-- To see current settings:
SHOW autocommit;In application code, ensure transactions are explicit:
// Node.js with pg
const client = await pool.connect();
try {
// Explicitly start a transaction
await client.query('BEGIN');
// Your operations here
await client.query('UPDATE users SET name = $1', ['Alice']);
// Explicitly commit
await client.query('COMMIT');
} catch (error) {
// Explicitly rollback on error
await client.query('ROLLBACK');
} finally {
client.release();
}Connection pooling can cause transaction state issues if connections are reused without proper cleanup.
Identify connection pool settings:
- Connection timeout (how long before an idle connection is dropped)
- Maximum pool size
- Idle timeout
Best practices:
// Node.js with pg - proper connection pooling
const pool = new Pool({
connectionTimeoutMillis: 5000, // 5 second connection timeout
idleTimeoutMillis: 30000, // 30 second idle timeout
max: 20, // Max 20 connections
});
// Always release connections when done
const client = await pool.connect();
try {
await client.query('BEGIN');
// ... your queries
await client.query('COMMIT');
} finally {
client.release(); // CRITICAL: Always release
}If a connection drops mid-transaction:
pool.on('error', (error, client) => {
console.error('Unexpected error on idle client', error);
// The client is no longer usable - it will be removed from the pool
});### Transaction State Diagram
PostgreSQL transactions have three states:
1. IDLE (no transaction)
- Ready to start a new transaction with BEGIN
- COMMIT or ROLLBACK here = Error 25P01
2. ACTIVE (transaction in progress)
- BEGIN was called
- Statements execute normally
- Can use SAVEPOINT
3. ABORTED (transaction failed)
- An error occurred in a statement
- All subsequent statements fail until ROLLBACK or ROLLBACK TO
- COMMIT will fail
You can check the transaction state in psql:
-- Check if you're in a transaction
SELECT current_transaction_isolation_level;
-- Check the entire session state
SELECT * FROM pg_stat_activity WHERE pid = pg_backend_pid();### Nested Transaction Behavior
PostgreSQL doesn't support true nested transactions, but implements them via SAVEPOINT. This is important for ORMs:
BEGIN;
-- Outer transaction active
UPDATE table1 SET col1 = 'value1';
SAVEPOINT sp1;
-- Subtransaction (nested) active
UPDATE table2 SET col2 = 'value2';
ROLLBACK TO sp1;
-- Back to outer transaction, table2 update rolled back
COMMIT;
-- Both transactions commitIf your ORM (Laravel's DB::transaction with nested calls) tries to use SAVEPOINT when no outer transaction is active, you'll get error 25P01.
### Connection Reset and Transaction Recovery
If a long-running transaction times out:
// Detecting a reset connection
const client = await pool.connect();
try {
await client.query('BEGIN');
// Long-running operation that times out
await client.query('SELECT pg_sleep(31)'); // Default timeout is usually 30s
await client.query('COMMIT'); // This will fail: ERROR 25P01
} catch (error) {
if (error.code === '25P01') {
console.error('Transaction was aborted, reconnecting...');
// Create a new client, the old one is no longer usable
}
}Always implement retry logic for transaction failures, especially in concurrent systems.
### Debugging with PostgreSQL Logs
Enable query logging to see the actual transaction state:
-- In postgresql.conf
log_statement = 'all' -- Log all statements
log_duration = on -- Log execution time
-- Then check /var/log/postgresql/postgresql.log for transaction sequence### Common ORM/Framework Fixes
TypeORM:
const queryRunner = connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(user);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}Knex.js:
await knex.transaction(async (trx) => {
await trx('users').insert({ name: 'Alice' });
// Transaction auto-commits on return
});insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL
ERROR 42501: must be owner of table
How to fix "must be owner of table" in PostgreSQL
trigger cannot change partition destination
How to fix "Trigger cannot change partition destination" in PostgreSQL
SSL error: certificate does not match host name
SSL error: certificate does not match host name in PostgreSQL
No SSL connection
No SSL connection to PostgreSQL