How do transactions work, and how do you use them in Node?
Quick Answer
A transaction groups multiple statements into an all-or-nothing unit (BEGIN → COMMIT, or ROLLBACK on error), giving ACID guarantees. In Node you acquire a single connection, run the statements on it, and commit or roll back — critical for multi-step operations like transfers where partial success would corrupt data.
Detailed Answer
Answer: A transaction makes several operations succeed or fail together, preserving data integrity. It provides ACID guarantees: Atomicity, Consistency, Isolation, Durability.
Classic example — money transfer (both updates must apply or neither):
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amt, from]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amt, to]);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK'); // undo everything on any failure
throw err;
} finally {
client.release(); // always return the connection
}
Key points:
- Use one connection for the whole transaction — you can't spread
BEGIN/COMMITacross pooled connections. - Always
ROLLBACKon error andrelease()infinally.
With an ORM (Prisma):
await prisma.$transaction([
prisma.account.update({ where: { id: from }, data: { balance: { decrement: amt } } }),
prisma.account.update({ where: { id: to }, data: { balance: { increment: amt } } }),
]);
Isolation levels (READ COMMITTED, REPEATABLE READ, SERIALIZABLE) control what concurrent transactions can see, trading strictness for concurrency. Higher levels prevent anomalies (dirty/non-repeatable/phantom reads) but can cause serialization failures you must retry.
When you need them: any multi-step write that must be consistent — orders + inventory, transfers, "create user + profile + audit log." Skipping transactions here leads to partial writes and corrupted state.