---
title: "트랜잭션 안에서 에러를 삼키면 COMMIT 이 조용히 ROLLBACK 된다 (node-postgres)"
description: "실패해도 되는 조회를 try/catch 로 삼켰더니 앞선 UPDATE 까지 사라졌습니다. aborted 트랜잭션의 COMMIT 이 에러 없이 ROLLBACK 이 되는 이유와 SAVEPOINT 로 막는 방법입니다."
date: 2026-10-14
updated: 2026-10-14T02:50:48.327Z
tags: [postgresql, node-postgres, transaction, error-handling]
canonical: https://blog.wooncloud.com/posts/node-postgres-swallowed-error-commit-rollback
---

파일을 휴지통으로 옮기는 상태 전이를 UPDATE 하는 트랜잭션 안에서, "읽어 두면 좋지만 실패해도 전이는 막지 않는" 조회를 하나 더 했습니다. `try/catch` 로 감싸고 실패하면 `null` 을 돌려주게 했습니다.

그런데 이 조회가 실패하는 순간, 앞서 한 UPDATE 까지 같이 사라졌습니다.

![삼킨 에러로 트랜잭션이 aborted 되면 COMMIT 이 조용히 ROLLBACK 되지만, SAVEPOINT 로 감싸면 UPDATE 는 그대로 남는다.](/images/posts/node-postgres-swallowed-error-commit-rollback/4a20506e-e8bf-45a1-83e1-7a2ff6ba0f91.webp)

## try/catch 로 감쌌는데 왜 사라질까요

PostgreSQL 은 트랜잭션 안에서 문장 하나가 실패하면 트랜잭션 전체가 aborted 상태가 됩니다. 이후 문장은 전부 `current transaction is aborted, commands ignored until end of transaction block` 으로 거부됩니다.

더 나쁜 건 마지막 `COMMIT` 입니다. aborted 트랜잭션에 `COMMIT` 을 보내면 서버는 에러 없이 `ROLLBACK` 을 수행하고, 명령 태그로 `ROLLBACK` 을 돌려줍니다. node-postgres 의 `client.query("COMMIT")` 은 예외를 던지지 않습니다.

![삼킨 에러 때문에 aborted 가 된 트랜잭션에서는 COMMIT 이 조용히 ROLLBACK 되어 UPDATE 가 사라진 채 성공 응답이 나가고, SAVEPOINT 로 감싸면 조회만 되돌려지고 UPDATE 는 남는다.](/images/posts/node-postgres-swallowed-error-commit-rollback/a2bcfe70-acfa-4dc6-a758-2db52788240a.svg)

그래서 앞선 UPDATE 는 통째로 사라지는데 애플리케이션은 성공 경로로 계속 갑니다. 응답도 성공이고, 검색 색인이나 감사 로그, 이벤트 같은 후속 부수효과는 "전이됨"으로 실행됩니다.

**실패해도 막지 않겠다는 의도와 정반대로, 실패가 본 작업을 조용히 취소한 셈입니다.**

## 가두거나 던지거나

방법은 둘 중 하나입니다.

- `SAVEPOINT` 로 감쌉니다. 실패해도 되는 문장만 부분 롤백합니다.
- 삼키지 말고 던집니다. 트랜잭션 전체를 실패시켜 호출자가 알게 합니다.

그리고 트랜잭션 헬퍼에서 `COMMIT` 결과의 `command` 를 확인해 `ROLLBACK` 이면 throw 하게 하면, 같은 종류의 조용한 유실을 한 곳에서 막을 수 있습니다.

```ts
// 나쁜 예: 삼키면 트랜잭션이 aborted 가 되고, COMMIT 이 ROLLBACK 이 된다
await client.query("BEGIN");
await client.query("UPDATE items SET status = 'trashed' WHERE id = $1", [id]);
let extra = null;
try { extra = (await client.query("SELECT ... possibly failing ...")).rows; } catch { /* ignore */ }
const r = await client.query("COMMIT"); // 예외 없음, r.command === "ROLLBACK"

// 좋은 예 1: SAVEPOINT 로 실패를 가둔다
await client.query("SAVEPOINT optional_read");
try {
  extra = (await client.query("SELECT ...")).rows;
  await client.query("RELEASE SAVEPOINT optional_read");
} catch {
  await client.query("ROLLBACK TO SAVEPOINT optional_read");
  extra = null;
}

// 좋은 예 2: 헬퍼에서 조용한 롤백을 잡는다
const res = await client.query("COMMIT");
if (res.command === "ROLLBACK") throw new Error("transaction was aborted; COMMIT rolled back");
```

에러 없이 성공하는 실패가 제일 찾기 어렵습니다. 트랜잭션 헬퍼에 `COMMIT` 검사를 넣어 두는 건 싸고 확실한 방어였습니다.

## 참고

- PostgreSQL SAVEPOINT: https://www.postgresql.org/docs/current/sql-savepoint.html
- PostgreSQL COMMIT: https://www.postgresql.org/docs/current/sql-commit.html
- PostgreSQL Transactions tutorial: https://www.postgresql.org/docs/current/tutorial-transactions.html
