빠른 흐름
try {
await fetchData();
} catch (err) {
console.error(err);
}
main().catch((err) => {
console.error("main failed:", err);
process.exit(1);
});에러 처리
async 에러 흐름에서 먼저 보는 대표 형태는 아래입니다.
await를 포함한try/catch- Promise 체인의
.catch() - 엔트리포인트
main().catch(...) - 마지막 안전망
unhandledRejection
await 누락은 가장 흔한 누수 지점
async function fetchData() {
throw new Error("network failed");
}
async function handler() {
try {
fetchData(); // await 없음
} catch (err) {
// 여기로 안 온다
}
}이 코드는 try/catch가 있어 보여도 실제로는 Promise를 기다리지 않습니다. 그래서 에러는 호출자 범위를 벗어나 unhandledRejection 쪽으로 흘러갈 수 있습니다.
catch 처리
.catch()도 에러를 끝까지 보내지 않을 수 있다
fetchUser(id)
.catch((err) => {
console.error(err);
})
.then((user) => {
console.log(user);
});여기서 catch가 값을 다시 던지지 않으면 체인은 fulfilled처럼 이어질 수 있습니다. 그래서 "로깅만 하고 끝"인 catch는 종종 더 위험합니다.
즉 "에러를 기록했다"와 "에러를 처리했다"는 다릅니다. 복구가 아니면 다시 throw하거나 실패 값을 명시적으로 반환해야 흐름이 분명해집니다.
엔트리포인트
최상위에서는 실패를 명시적으로 닫는다
async function main() {
await doWork();
}
main().catch((err) => {
console.error("main failed:", err);
process.exit(1);
});Node에서는 이 패턴이 중요합니다.
- entrypoint Promise는 반드시 닫는다
- 처리되지 않은 rejection은 마지막 안전망으로 로깅/정리 후 종료 방향을 정한다
unhandledRejection은 복구 지점보다 추적 지점에 가깝다
이 이벤트에서 뭔가를 수습해 서비스 정상 흐름으로 되돌리기보다, 왜 rejection이 여기까지 새었는지 추적하는 편이 더 중요합니다. 핵심은 안전망 자체보다 rejection이 여기까지 새지 않게 에러 경로를 닫는 데 있습니다.
현재 Node는 --unhandled-rejections 기본값이 throw이므로, 처리되지 않은 rejection이 uncaught exception 경로로 이어질 수 있습니다. 그래서 "경고만 찍히고 끝난다"는 예전 감각으로 읽지 않는 편이 안전합니다. 다만 실제 동작은 플래그 설정에도 영향을 받으므로, 운영 환경에서 어떤 모드로 실행하는지도 함께 확인해야 합니다.
어디서 끊어야 하나
체크포인트
- async 호출이면
await여부 먼저 본다 - catch가 다시 throw하는지 본다
- 최상위 Promise는
.catch()로 닫는다 unhandledRejection은 누락 추적용 안전망- 로깅만 하고 끝나는 catch인지도 같이 본다
주의할 점
unhandled rejection을 "나중에 로그만 찍고 넘어가는 경고"처럼 다루면 장애가 지연된 형태로 남습니다. Node 앱에서는 이 이벤트가 보였다는 사실 자체가 에러 경로 설계가 비어 있다는 뜻에 더 가깝습니다.
참고 링크
2 sources