개념/배경

Node.js 프로세스 레벨에서 잡히지 않은 에러를 마지막으로 처리하는 안전망을 두는 목적 정리 애플리케이션 어디에서도 처리되지 못한 오류를 기록하고 종료 경로를 일관되게 관리하기 위함 프로덕션 환경에서 원인 파악과 사후 조치 자동화를 위한 최소 장치로 간주

핵심 이벤트

두 가지 이벤트가 글로벌 핸들러의 대상

  • uncaughtException
    • try-catch로 포착되지 않은 동기 오류가 호출 스택 끝까지 전파될 때 발생
    • 핸들러가 없으면 프로세스가 비정상 종료됨
// 동기 에러가 상위에서 잡히지 않은 경우
function boom() {
  throw new Error('폭발')
}
boom()
  • unhandledRejection
    • Promise가 reject되었으나 await 혹은 .catch로 처리되지 않은 경우 발생
    • Node.js 15+ 기본 동작은 핸들러가 없을 때 프로세스 종료
// reject가 소비되지 않은 경우
async function asyncBoom() {
  throw new Error('비동기 폭발')
}
asyncBoom()

왜 글로벌 핸들러인가

코드 최상위에서 아무도 잡지 못한 에러에 대한 마지막 안전망 역할

  • 통일된 포맷으로 치명적 오류 로깅
  • 시간, 환경, 실행 컨텍스트 등 추가 맥락 기록 용이
  • 알림 전송, 임시 리소스 정리 같은 최소 후처리 수행
  • 종료 코드 일관화 및 관찰성 강화

예시

핸들러가 없을 때의 기본 동작은 스택트레이스 출력 후 종료

// main.ts
async function bootstrap() {
  throw new Error('부트스트랩 실패')
}
bootstrap()

출력 예

/Users/.../main.ts:2
  throw new Error('부트스트랩 실패')
        ^
Error: 부트스트랩 실패
    at bootstrap (/Users/.../main.ts:2:9)
    ...

핸들러를 둘 경우 포맷을 통일해 기록 후 종료 가능

process.on('uncaughtException', (error) => {
  console.log('='.repeat(50))
  console.log('[FATAL] 작업이 비정상 종료 예정')
  console.log('[FATAL] Error:', error && error.message)
  console.log('[FATAL] Stack:', error && error.stack)
  console.log('[FATAL] 시간:', new Date().toISOString())
  console.log('='.repeat(50))
  process.exit(1)
})

async function bootstrap() {
  throw new Error('부트스트랩 실패')
}
bootstrap()

출력 예

==================================================
[FATAL] 작업이 비정상 종료 예정
[FATAL] Error: 부트스트랩 실패
[FATAL] Stack: Error: 부트스트랩 실패
    at bootstrap (...)
[FATAL] 시간: 2025-12-03T14:30:00.000Z
==================================================

동작 원리 요약

  • uncaughtException

    • 에러 발생 → 호출 스택 전파 → 어느 곳에서도 catch되지 않으면 이벤트 발생
    • 핸들러 없으면 종료, 핸들러 있으면 개발자 로직 실행 후 보통 종료 선택
  • unhandledRejection

    • Promise reject → 소비되지 않음 → 이벤트 발생
    • Node.js 15+에서 핸들러 없으면 기본적으로 종료

주의사항

  • 복구 시도 금지
    • 프로세스가 불안정 상태일 수 있으므로 로깅 후 종료 권장
  • process.exit 호출 누락 금지
    • 종료하지 않으면 누수와 이상 상태 지속 위험
  • 최소 작업만 수행
    • 동기 로깅, 메트릭 전송 시 타임아웃 짧게, 복잡한 비동기 작업 지양
  • unhandledRejection 처리 시 주의
    • reason이 Error가 아닐 수 있음 → 안전하게 문자열화하여 로깅
    • 정책 일관화를 위해 핸들러에서 에러를 throw하거나 process.exit로 종료 권장
  • 핸들러 중복 등록 방지
    • 초기화 구간에서 process.once 사용 고려
  • 종료 코드 일관화
    • 치명적 오류는 1 같은 비영 0 종료 코드 사용

간단 스니펫

process.once('unhandledRejection', (reason) => {
  const msg = reason instanceof Error ? reason.stack : String(reason)
  console.error('[FATAL] Unhandled Rejection\n', msg)
  process.exit(1)
})

process.once('uncaughtException', (err) => {
  console.error('[FATAL] Uncaught Exception\n', err && err.stack)
  process.exit(1)
})

베스트 프랙티스

  • 애플리케이션 레벨에서 먼저 잡는다
    • 각 경계에서 try-catch, Promise 체인에 .catch, 최상위 await에 try-catch 배치
  • 글로벌 핸들러는 마지막 안전망으로만 사용
  • 로깅 포맷을 고정하고 관찰성 도구와 연동
    • 시간, 서비스명, 버전, 요청 식별자 등 가능하면 포함
  • 종료 전 단명 정리만 수행
    • 열려 있는 로그 버퍼 flush 시도, 임시 파일 제거 정도로 한정

요약

  • 정의: 프로세스 레벨의 마지막 에러 핸들러
  • 대상: uncaughtException, unhandledRejection
  • 필요성: 원인 추적과 후처리 일원화, 프로덕션 안정성 확보
  • 운영 원칙: 로깅하고 빠르게 종료, 복구 시도하지 않음

비유

try-catch = 각 방의 소화기
글로벌 핸들러 = 건물 스프링클러, 최종 안전망

참고자료