본문으로 건너뛰기
개발 머꼬
개발 노트Node.js
hohyeon.dev19

await를 하나 빠뜨렸더니 서버 프로세스가 통째로 죽은 이유

  • #Debugging
  • #Engineering Note
  • #Node.js

문제 발생

배포 후 서버가 하루에 몇 번씩 재시작됐습니다. 로그에는 이것만 남았습니다.

[UnhandledPromiseRejection] This error originated either by throwing inside of an
async function without a catch block, or by rejecting a promise which was not handled

원인은 await를 빠뜨린 한 줄이었습니다.

sendWelcomeEmail(user);   // await 없음 — 실패해도 아무도 잡지 않는다

원인 분석

Node는 더 이상 조용히 넘어가지 않습니다. 문서가 그대로 적습니다 — 'unhandledRejection' 이벤트가 발생했는데 처리되지 않으면 uncaught exception으로 던져집니다. 이 동작은 --unhandled-rejections 플래그로 바꿀 수 있습니다. 기본 모드가 throw입니다.

예전 Node에서는 경고만 찍고 지나갔기 때문에, 오래된 코드가 그대로 있으면 런타임을 올린 뒤 이렇게 드러납니다.

여기서 흔한 잘못된 처방이 process.on("unhandledRejection", () => {})로 삼켜버리는 것입니다. 문서는 비슷한 상황인 'uncaughtException'에 대해 강하게 경고합니다 — 이는 최후의 수단으로만 쓰도록 의도된 조악한 예외 처리 장치이며, "On Error Resume Next"의 등가물로 쓰여서는 안 됩니다. 처리되지 않은 예외는 본질적으로 애플리케이션이 정의되지 않은 상태에 있다는 뜻입니다. 그리고 'uncaughtException' 이후 정상 동작을 재개하는 것은 안전하지 않습니다.

해결 방안

  1. 부수 작업이라도 결과를 처리합니다.
await sendWelcomeEmail(user);                       // 실패가 요청 결과에 반영되어야 하면
void sendWelcomeEmail(user).catch(reportError);     // 배경 작업이면 최소한 catch
  1. Next.js라면 응답 이후 작업은 after()가 제자리입니다. 실행 보장과 오류 처리 지점이 함께 생깁니다.

  2. 로깅 훅은 붙이되 삼키지 않습니다. 원인을 남기고 프로세스는 정리 후 종료시킵니다.

process.on("unhandledRejection", (reason) => {
  logger.fatal({ reason }, "unhandled rejection");
  process.exit(1);
});
  1. 재시작은 프로세스 매니저에 맡깁니다. 문서 권고 그대로 — 애플리케이션 실패를 감지해 복구하거나 재시작하려면 별도 프로세스의 외부 모니터를 두어야 합니다. Docker의 restart 정책이나 systemd가 그 역할입니다.

  2. 린트로 미리 잡습니다. no-floating-promises(typescript-eslint) 규칙이 await 누락을 컴파일 단계에서 잡아줍니다 — 이 버그를 런타임까지 가져가지 않는 가장 값싼 방법입니다.

  3. --unhandled-rejections=warn으로 되돌리지 않습니다. 증상만 감추고, 정작 그 요청은 이미 잘못된 상태로 끝나 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.