Node.js 교재 · 16편 · 에러 처리

에러 처리 — try/catch와 Express 에러

예외 한 줄로 프로세스가 죽으면 안 된다. 안전망 만드는 4가지 층.

에러가 try/catch 와 미들웨어로 잡히는 컨셉 일러스트

실전 백엔드의 70% 는 에러 처리. 정상 흐름은 짧고 깔끔한데, "DB 끊김"·"외부 API 타임아웃"·"잘못된 입력"·"메모리 부족" 같은 비정상이 무한히 많다. 한 군데 빠뜨리면 사용자는 500 에러를 보고, 최악의 경우 프로세스가 통째로 죽는다.

Node 에서 에러를 다루는 도구는 4가지 — try/catch · 커스텀 에러 클래스 · Express 에러 미들웨어 · uncaughtException 안전망. 이번 편에서 네 층을 쌓아 진짜 단단한 서비스로 만든다.

1. try/catch — 기본 안전망

9편의 async/await 와 함께 가장 자주 쓰는 패턴.

async function fetchUserData(id) { try { const user = await db.users.findUnique({ where: { id } }); if (!user) throw new Error('사용자 없음'); return user; } catch (err) { console.error('[fetchUserData]', err); throw err; // 호출자에게 다시 던짐 (또는 기본값 반환) } }

중요한 결정 — catch 안에서 throw 다시 할지, 기본값 반환할지, 로깅만 할지. 셋 다 정당한 선택. 단 무엇을 골랐는지 의식적으로.

흔한 안티패턴 — 빈 catchcatch (err) {} 처럼 비워두면 에러가 통째로 사라진다. 디버깅 지옥. 최소한 console.error 또는 로깅 호출이라도. 무시할 거면 명시적으로 // intentionally ignored 주석.

2. 커스텀 에러 클래스 — 분류와 status 매핑

모든 에러가 같지 않다. "찾을 수 없음"·"권한 없음"·"검증 실패" 는 서로 다른 status 와 메시지가 필요. Error 를 상속해 분류한다.

// errors.js export class AppError extends Error { constructor(message, status = 500, code = 'INTERNAL') { super(message); this.status = status; this.code = code; this.name = this.constructor.name; } } export class NotFoundError extends AppError { constructor(resource = '리소스') { super(`${resource}을 찾을 수 없습니다`, 404, 'NOT_FOUND'); } } export class ValidationError extends AppError { constructor(details) { super('입력값이 잘못되었습니다', 400, 'VALIDATION_FAILED'); this.details = details; } } export class UnauthorizedError extends AppError { constructor() { super('로그인이 필요합니다', 401, 'UNAUTHORIZED'); } }

사용 — 핸들러 안에서 그냥 throw.

router.get('/posts/:id', async (req, res, next) => { const post = await db.posts.findUnique({ where: { id: req.params.id } }); if (!post) throw new NotFoundError('글'); // ← 한 줄 res.json(post); });

이 한 줄이 status 404 + 메시지 + code 까지 자동으로 전달한다. 중앙 에러 핸들러가 그걸 받아 응답.

3. Express 에러 미들웨어 — 중앙 처리

14편에서 잠깐 본 패턴. 인자 4개가 신호.

// server.js — 라우트 모두 등록 뒤에 app.use((err, req, res, next) => { // 1. 로깅 (구조화된 형식으로) console.error({ name: err.name, code: err.code, message: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, path: req.path, method: req.method, }); // 2. 알려진 AppError 면 그대로 응답 if (err instanceof AppError) { return res.status(err.status).json({ error: { code: err.code, message: err.message, details: err.details }, }); } // 3. 모르는 에러는 500 + 안전 메시지 res.status(500).json({ error: { code: 'INTERNAL', message: '서버 오류가 발생했습니다' }, }); });

핵심 — 알려진 에러와 모르는 에러를 나눈다. AppError 는 사용자에게 그대로 보여줘도 안전한 메시지. 그 외(TypeError·RangeError 등)는 디버깅 정보가 들어있을 수 있으니 안전한 일반 메시지로 교체.

스택 트레이스 노출 금지 — production 에서 err.stack 을 응답에 포함하면 코드 경로·라이브러리 버전이 새 나간다. 보안 사고. 로깅엔 포함, 응답에선 제거. NODE_ENV === 'development' 분기가 표준.

4. async 미들웨어 함정 — Express 5 vs 4

14편에서도 짚었던 부분. 다시 한 번.

// ❌ Express 4 — async throw 자동 캐치 안 됨 router.get('/x', async (req, res) => { throw new Error('won\\'t be caught!'); }); // ✅ Express 5 (2024 stable) — 자동 캐치 router.get('/x', async (req, res) => { throw new Error('caught by error middleware'); }); // ✅ Express 4 호환 — 매 라우트 try/catch 또는 wrap 헬퍼 const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); router.get('/x', asyncH(async (req, res) => { throw new Error('caught'); }));

Express 5 면 그냥 throw, 4 면 wrap 헬퍼 또는 express-async-errors 패키지. 어느 버전인지 package.json 확인부터.

5. uncaughtException — 최후의 안전망

모든 try/catch 와 미들웨어가 잡지 못한 에러가 결국 여기로. 잡지 않으면 프로세스가 죽는다.

// server.js 맨 위 process.on('uncaughtException', (err) => { console.error('[uncaughtException]', err); // 외부 에러 추적 서비스로 보고 (Sentry 등) // 권장: 안전하게 종료 후 PM2/Docker 가 재시작 process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('[unhandledRejection]', reason); // Node 22 부터 기본이 throw 라 결국 uncaughtException 으로 });
uncaughtException 후 process.exit(1) 권장 — 표면적으론 "어차피 잡았으니 계속 돌아도 되지 않나?" 싶지만, 그 시점에 메모리·소켓 상태가 깨졌을 가능성이 있다. 안전하게 종료 후 PM2·Docker·systemd 가 재시작하는 게 production 표준. 22편 PM2 에서 다시 다룬다.

요약 — 16편 좌표

여기까지 정리. 에러 처리 4층 — ① try/catch(직접 잡기, 빈 catch 금지), ② 커스텀 AppError(분류·status·code), ③ Express 에러 미들웨어(인자 4개, 알려진/모르는 에러 분기, 스택 노출 금지), ④ uncaughtException(최후 안전망, exit 1 + 재시작). Express 4 면 async wrap, 5 면 자동. 다음 편에서 환경변수 다루기.

다음 편 예고 — 환경변수와 dotenv

.env 로 비밀 설정 분리, NODE_ENV 활용. 17편.

© 2026 주나이테크(주) @JUNAITECH