Node.js uncaughtException 처리: 처리되지 않은 예외로 프로세스가 종료되는 문제

1. 문제 정의

Node.js를 처음 사용하다 보면 프로그램에서 **처리되지 않은 예외(unhandled exception)**가 발생하는 순간 Node 프로세스 전체가 종료되는 것을 발견하게 됩니다.

1
2
3
4
5
// 어떤 라우트/콜백에서 던져진 예외
if (conditionFailed) {
  throw new Error("something went wrong");
}
// -> 프로세스가 그대로 crash

일반 웹 서버 컨테이너라면 Worker Thread만 죽고 컨테이너는 계속 요청을 받지만, Node.js에서는 프로세스가 통째로 죽습니다. 이때 자연히 다음과 같은 질문이 생깁니다.

  • process.on('uncaughtException')이 유일한 방어 수단인가?
  • 이 핸들러를 쓰는 것이 안전한가?

2. 원인 탐구

Node.js의 동작 모델을 이해하면 프로세스 종료의 이유가 명확해집니다.

  • Node.js는 **단일 이벤트 루프(single event loop)**를 가진 프로세스입니다.
  • 사전에 예외를 잡아둔 핸들러가 없다면, 예외는 이벤트 루프의 맨 위까지 전파되고 결국 실행 환경이 더 이상 안전하게 실행을 계속할 수 없다고 판단합니다.
  • 따라서 Node는 충돌을 방지하기 위해 프로세스를 종료합니다. 원 질문의 스코어가 831점으로 매우 높을 만큼, 이 패턴에 직면한 개발자가 많은 공통 이슈입니다.

3. 근본 원인 분석

핵심 원인은 **‘예외가 전파되어 이벤트 루프를 핸들러 없는 상태로 통과했기 때문’**입니다.

  • process.on('uncaughtException')는 존재하지만, 공식 문서에서는 이를 만병통치로 쓰지 말 것을 경고합니다. 핸들러는 “마지막 순간에 상태를 정리하고 종료하거나, 안전하게 종료 절차를 밟기 위한” 용도입니다.
  • 진짜 해결책은 예외를 구조적으로 미리 잡는 것으로, 비동기 콜백의 err 첫 번째 인자 규약과 Promise 기반 에러 전파를 사용하는 방식입니다.

Joyent의 공식 가이드(StackOverflow 채택 답변 요약)는 “safely throwing errors"라는 개념을 제시합니다.

4. 코드 해결책

안전하게 ‘던지기’ — 동기 코드

동기 함수에서 오류가 나면, throw 대신 오류를 반환하는 방식입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 동기 함수에서 오류를 throw 대신 return으로 반환
var divideSync = function (x, y) {
  if (y === 0) {
    // 오류 조건이면 Error 객체를 반환 (throw 아님)
    return new Error("Can't divide by zero");
  }
  return x / y;
};

var result = divideSync(4, 2); // 2
if (result instanceof Error) {
  console.log("4/2=err", result);
} else {
  console.log("4/2=", result);
}

result = divideSync(4, 0); // Error
if (result instanceof Error) {
  console.log("4/0=err", result);
}

콜백 기반 코드 — err-first 규약

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var divide = function (x, y, next) {
  if (y === 0) {
    // 콜백의 첫 번째 인자가 err
    return next(new Error("Can't divide by zero"));
  }
  next(null, x / y);
};
divide(4, 2, function (err, result) {
  if (err) return console.error(err); // err 처리
  console.log("4/2=", result);
});

이벤트 이미터 / Promise로 내보내기

에러를 emit('error')로 내보내거나 Promise reject로 처리해도, 안전하게 전파되고 uncaughtException까지 새지 않습니다.

5. 향후 예방 조치

  • process.on('uncaughtException')절대 ‘모든 예외 무시’로 쓰지 마십시오. 안전한 사용법은 로그를 남기고 프로세스를 깨끗이 종료하는 것입니다.
  • 모든 IO는 try/catch 또는 .catch()를 통해 실패 경로를 명시하십시오.
  • worker처럼 격리된 프로세스/Worker Thread에서 취약한 작업을 수행하면, 실패해도 메인이 종료되지 않게 만들 수 있습니다.
  • 예외를 함수의 실패 반환으로 변환하는 “safely throwing” 패턴을 코드베이스 규약으로 정하십시오.

출처: StackOverflow — Node.js Handling Uncaught Exceptions