코드를 짜다 보면 에러는 반드시 난다. 서버가 죽고, 사용자가 이상한 값을 넣고, 있어야 할 데이터가 없다. 초보 시절 나는 에러를 부끄러운 실수로 여겨 어떻게든 안 나게 막으려고만 했다. 실무를 하며 생각이 바뀌었다. 에러는 없앨 수 있는 게 아니라 다뤄야 하는 대상이다. 잘 다룬 에러는 프로그램을 멈추는 대신, 무엇이 왜 틀어졌는지 알려주고 다음 행동을 정하게 해준다.


이번 편은 에러를 던지는 throw, 잡는 try catch, 뒷정리를 맡는 finally, 상황에 맞춘 커스텀 에러, 비동기 코드의 에러 처리까지 다룬다. 예제는 대부분 Node.js(자바스크립트 실행 환경)로 그대로 돌려 결과를 확인한 것들이다. 눈으로만 읽지 말고 직접 실행해보면 훨씬 빨리 익는다.


에러를 던지고 잡는다. throw로 에러를 던지면 함수 실행이 그 자리에서 뚝 멈추고, 제어가 가장 가까운 try catch로 튀어 올라간다. 던지는 값은 보통 new Error("메시지")로 만든 에러 객체다.


function parseAge(input) {
const n = Number(input);
if (Number.isNaN(n)) throw new Error("숫자가 아님: " + input);
if (n < 0) throw new RangeError("나이는 음수일 수 없음");
return n;
}
try {
console.log(parseAge("30"));
console.log(parseAge("서른"));
} catch (err) {
console.log("잡음:", err.name, "-", err.message);
} finally {
console.log("항상 실행");
}


이 코드를 실행하면 이렇게 나온다.


30
잡음: Error - 숫자가 아님: 서른
항상 실행


첫 호출은 정상이라 30이 찍혔다. 둘째 호출에서 throw가 터지자 그 줄에서 실행이 멈추고 catch로 넘어갔다. 그래서 둘째 console.log는 아예 실행되지 않았다. 에러 객체의 err.message에는 내가 넣은 문자열이, err.name에는 에러 종류가 들어 있다.


finally는 무조건 실행된다. 위 결과 마지막 줄 항상 실행이 그 증거다. finally 블록은 try가 성공하든 catch로 빠지든, 심지어 중간에 return을 하든 상관없이 반드시 돈다. 그래서 파일을 닫거나 로딩 표시를 끄는 것처럼, 성공 실패와 무관하게 꼭 해야 하는 뒷정리를 여기 넣는다. 나는 로딩 스피너 끄는 코드를 finally에 넣고부터, 에러 났을 때 스피너가 영영 도는 버그를 안 겪는다.


에러에도 종류가 있다. 기본 Error 말고도 잘못된 타입엔 TypeError, 허용 범위를 벗어나면 RangeError 같은 게 자동으로 던져진다. 앞 예제에서 음수에 RangeError를 던진 것도 그 관례를 따른 거다. 어떤 종류인지는 err.name으로 보거나 err instanceof TypeError처럼 확인한다. 이 구분이 있어야 에러마다 다르게 대응할 수 있다.


커스텀 에러를 만든다. 실무에선 기본 에러만으론 부족하다. 어느 입력 필드가 문제인지 같은 정보를 함께 실어 보내고 싶기 때문이다. Error를 상속해 나만의 에러를 만들면 된다.


class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function checkForm(form) {
if (!form.email) throw new ValidationError("이메일 필수", "email");
return true;
}
try {
checkForm({ email: "" });
} catch (err) {
console.log("검증 실패:", err.field, "/", err.message);
}


실행 결과는 이렇다.


검증 실패: email / 이메일 필수


여기서 super(message)는 부모인 Error에게 메시지를 넘기는 호출인데, 이걸 빠뜨리면 message가 안 채워진다. this.field처럼 정보를 얹어두면, 잡는 쪽에서 어느 항목이 틀렸는지 정확히 알고 그 입력칸을 빨갛게 칠할 수 있다. 나는 폼 검증에 이 방식을 쓰고부터, 어디가 틀렸는지 사용자에게 콕 집어 알려줄 수 있게 됐다.


못 잡을 에러는 다시 던진다. 흔한 실수가 catch에서 모든 에러를 뭉뚱그려 삼키는 거다. 내가 예상한 에러만 처리하고, 모르는 에러는 위로 다시 던져야 한다.


try {
checkForm(form);
} catch (err) {
if (err instanceof ValidationError) {
showFieldError(err.field);
} else {
throw err; // 내가 모르는 에러는 위로 넘긴다
}
}


나는 종류를 안 가리고 다 처리한 셈 치다가, 오타로 생긴 진짜 버그까지 검증 실패 메시지로 덮은 적이 있다. 화면엔 이메일을 확인하라 떴는데 실은 코드가 깨진 거라 원인 찾는 데 며칠이 걸렸다. 아는 에러만 처리하고 나머지는 정직하게 위로 넘기는 게 원칙이다.


프로미스 에러는 catch 메서드로. 비동기로 넘어가면 에러가 throw가 아니라 프로미스의 거부로 온다. 이건 catch 메서드로 잡는다.


function loadUser(id) {
return new Promise((resolve, reject) => {
if (id <= 0) reject(new Error("잘못된 id"));
else resolve({ id, name: "kim" });
});
}
loadUser(-1)
.then(u => console.log("성공:", u))
.catch(err => console.log("실패:", err.message));


실행하면 실패: 잘못된 id가 찍힌다. 거부된 프로미스는 then을 건너뛰고 곧장 catch로 흐른다. 체인 끝에 catch를 안 달면 이 에러는 처리 안 된 거부가 되어 조용히 묻히니, 프로미스 체인 끝엔 catch를 습관처럼 붙여야 한다.


async 에러는 try catch로. async await를 쓰면 비동기 에러도 동기 코드처럼 평범한 try catch로 잡힌다.


async function main() {
try {
const u = await loadUser(0);
console.log(u);
} catch (err) {
console.log("async에서 잡음:", err.message);
}
}
main();


결과는 async에서 잡음: 잘못된 id다. await한 프로미스가 거부되면 그 자리에서 throw가 일어난 것처럼 동작해 catch로 잡힌다. 동기 에러든 비동기 에러든 같은 try catch 하나로 통일되니, 나는 이 문법의 에러 처리가 특히 마음에 든다.


빈 catch는 최악이다. 초보 코드에서 제일 자주 보이는 안티패턴이 아무것도 안 하는 catch (err) {}다. 에러를 잡긴 잡는데 아무 반응도 없이 그냥 삼킨다. 그러면 문제가 생겨도 화면은 멀쩡한 척, 로그도 조용해서 원인을 추적할 실마리가 통째로 사라진다. 나는 남의 빈 catch 때문에 없는 버그를 몇 시간 쫓은 적이 여러 번이다. 최소한 console.error로라도 남겨야, 나중에 무슨 일이 있었는지 알 수 있다.


finally에서 return하지 마라. 마지막으로 은근히 사람 잡는 함정 하나. finally 안에서 return을 하면, trycatch가 돌려주려던 값을 덮어써 버린다.


function f() {
try {
return "정상";
} finally {
return "덮어씀";
}
}
console.log(f()); // "덮어씀"


try에서 정상을 반환하려 했는데 결과는 덮어씀이 나온다. finallyreturn이 최종 반환값을 가로챈 거다. 심지어 던지려던 에러까지 이렇게 삼켜져 사라진다. finally는 뒷정리만 하고, return은 절대 넣지 않는 게 안전하다.


정리하면 에러는 throw로 던지고 try catch로 잡으며, finally는 무조건 도는 뒷정리 자리다. 상황에 맞는 정보는 커스텀 에러에 실어 보내고, 모르는 에러는 삼키지 말고 위로 다시 던진다. 비동기 에러는 catch 메서드나 try catch로 받는다. 에러를 다루는 실력은 결국 실패를 얼마나 정직하게 드러내느냐에 달려 있다. 조용히 삼킨 에러는 반드시 더 나쁜 모습으로 돌아온다.