에러 처리 — try·catch·finally·throw
예상 못한 일이 일어났을 때 코드가 무너지지 않게 하는 도구.
네트워크가 끊기고, 사용자가 이상한 값을 넣고, 파일이 없고 — 코드 작성자가 예상하지 못한 일은 항상 일어납니다. 그걸 우아하게 다루는 표준 도구가 try·catch·throw. 13편은 입문~기초 파트의 마지막 — 다음부터는 중급 파트(DOM·비동기·API)로 들어갑니다.
왜 에러 처리가 필요한가
// ❌ 에러 한 줄이 전체 페이지 죽임
const data = JSON.parse(userInput); // 입력이 잘못된 JSON 이면 SyntaxError
console.log(data.name); // 이 줄도, 그 아래도 모두 실행 안 됨
// ✅ try/catch 로 격리
let data;
try {
data = JSON.parse(userInput);
} catch (err) {
console.error("JSON 파싱 실패:", err.message);
data = {}; // 기본값으로 fallback
}
console.log(data.name); // 정상 실행
try·catch·finally 기본
try {
// 1) 위험할 수 있는 코드
risky();
} catch (err) {
// 2) 에러 발생 시만 실행
console.error(err);
} finally {
// 3) 성공·실패 무관하게 항상 실행 (정리·로깅)
cleanup();
}
finally 의 진가. 파일 닫기, DB 연결 해제, 로딩 스피너 끄기 — 성공해도 실패해도 반드시 해야 하는 정리 작업에 씁니다. catch 안에서 또 throw 해도 finally 는 실행됨.
Error 객체 — 기본 구조
const err = new Error("뭔가 잘못됨");
err.name // "Error"
err.message // "뭔가 잘못됨"
err.stack // 호출 스택 문자열 (디버깅의 핵심)
// 표준 내장 에러 클래스들
new TypeError("문자열 기대했는데 숫자");
new RangeError("범위 벗어남");
new SyntaxError("문법 오류");
new ReferenceError("선언 안 된 변수");
new URIError("encodeURI 실패");
throw — 직접 에러 발생
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("숫자여야 함");
}
if (b === 0) {
throw new RangeError("0 으로 나눌 수 없음");
}
return a / b;
}
try {
divide(10, 0);
} catch (err) {
console.error(err.name, err.message);
// "RangeError" "0 으로 나눌 수 없음"
}
문자열 throw 금지. throw "에러" 같은 패턴 가끔 보이지만 — stack trace 가 사라집니다. 디버깅이 거의 불가능해져요. 항상 throw new Error(...) 또는 표준 서브클래스.
커스텀 에러 클래스
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} #${id} not found`);
this.name = "NotFoundError";
this.resource = resource;
this.id = id;
}
}
// 사용
function findUser(id) {
const u = users.get(id);
if (!u) throw new NotFoundError("User", id);
return u;
}
try {
findUser(999);
} catch (err) {
if (err instanceof NotFoundError) {
// 404 처리
} else if (err instanceof ValidationError) {
// 400 처리
} else {
// 알 수 없는 에러 — 다시 던지기
throw err;
}
}
에러 다시 던지기 (re-throw) — 핵심 패턴
try {
loadConfig();
} catch (err) {
if (err.code === "ENOENT") {
// 파일 없으면 기본 설정으로
return defaultConfig();
}
// 그 외 에러는 호출자가 처리하도록 다시 던짐
throw err;
}
한 줄 원칙. "내가 모르는 에러는 잡지 마라." 모르는 에러를 catch 만 하고 무시하면(catch {}) 진짜 버그가 조용히 사라집니다. 잡을 수 있는 종류만 잡고 나머지는 던지기.
비동기 에러 — async/await 와 함께
// fetch 같은 비동기 — 똑같이 try/catch
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("로드 실패:", err);
return null;
}
}
// Promise.then 으로는 .catch
fetch("/api/x")
.then(r => r.json())
.catch(err => console.error(err));
비동기·Promise 의 자세한 패턴은 15·16편(비동기 입문·Promise/async).
unknown 으로 받기 (TS) / instanceof 좁히기
try {
doSomething();
} catch (err) {
// err 의 타입은 unknown (또는 any)
// 어떤 게 던져졌는지 모름 → 검사
if (err instanceof TypeError) {
console.error("타입 에러:", err.message);
} else if (err instanceof Error) {
console.error("일반 에러:", err.message);
} else {
console.error("이상한 것 던져짐:", err);
}
}
에러 처리 안티패턴 5가지
1. 빈 catch. catch {} 또는 catch(e) {} — 에러가 조용히 사라져 진짜 버그를 숨김.
2. catch 에서 console.log 만. 에러 발생을 사용자/모니터링에 알리지 않으면 안 일어난 것과 같음.
3. 모든 함수에 try/catch. 어차피 다시 던질 거면 가장 위(엔드포인트·이벤트 핸들러) 에서 한 번에 처리.
4. 에러를 boolean 으로 변환. try { ... return true } catch { return false } 같은 패턴은 어떤 에러인지 정보를 다 버림.
5. 너무 일반적인 메시지. "Error occurred" 같은 메시지는 도움 안 됨. 무엇이·어디서·왜 가 보이게.
전역 에러 핸들러 — 마지막 그물
// 브라우저
window.addEventListener("error", (event) => {
console.error("처리 안 된 에러:", event.error);
// Sentry·DataDog 같은 모니터링으로 전송
});
window.addEventListener("unhandledrejection", (event) => {
console.error("처리 안 된 Promise:", event.reason);
});
// Node
process.on("uncaughtException", (err) => { ... });
process.on("unhandledRejection", (reason) => { ... });
14편 — DOM 조작 (중급 시작)
querySelector · 이벤트 리스너 · 이벤트 위임. 입문~기초가 끝나고 진짜 웹페이지를 만집니다.
이전: 12편 스코프 · 현재: 13편 (기초 마지막) · 다음 → 14편 DOM (중급 시작) · 진행: 13/26