throw와 try/catch/finally — 에러 전파의 기초
throw 문의 동작 원리, try/catch/finally의 실행 순서, 에러를 재통(rethrow)해야 하는 이유를 정리합니다. 에러 처리의 가장 기본이 되는 메커니즘입니다.
지난 글에서 비동기 흐름 제어 패턴을 살펴봤습니다. 이번에는 JavaScript 에러 처리의 가장 기본인 throw와 try/catch/finally의 동작 원리를 정확히 짚어봅니다.
throw — 어떤 값이든 던질 수 있다
throw는 표현식이 아니라 **문(statement)**입니다. 어떤 값이든 던질 수 있지만, 항상 Error 객체(또는 그 서브클래스)를 던져야 합니다.
throw new Error('메시지'); // 권장
throw new TypeError('타입 불일치'); // 내장 서브클래스
throw '문자열'; // 비권장 — 스택 트레이스 없음
throw 42; // 비권장
문자열이나 숫자를 던지면 catch (e)에서 e.stack을 사용할 수 없어 디버깅이 어려워집니다. Error 객체를 던지면 발생 위치와 호출 스택이 자동으로 기록됩니다.
에러 전파 — 콜 스택을 거슬러 올라간다
throw된 에러는 현재 실행 컨텍스트를 즉시 빠져나가 catch를 만날 때까지 콜 스택을 거슬러 올라갑니다.
function parseJSON(text) {
if (!text.startsWith('{')) throw new SyntaxError('잘못된 JSON');
return JSON.parse(text);
}
function loadConfig(path) {
const text = readFile(path);
return parseJSON(text); // parseJSON에서 throw → 여기로 전파
}
function main() {
try {
const config = loadConfig('./config.json');
start(config);
} catch (e) {
// parseJSON에서 던진 에러가 여기서 포착됨
console.error('설정 로드 실패:', e.message);
}
}
중간에 catch 없이 에러를 그대로 통과시키는 것을 **에러 전파(propagation)**라 합니다. 각 함수가 에러를 처리할 필요 없이, 의미 있는 컨텍스트가 있는 곳에서 포착합니다.
try/catch/finally 실행 순서
function example() {
try {
console.log('1: try');
throw new Error('oops');
console.log('2: 실행 안 됨');
} catch (e) {
console.log('3: catch', e.message);
return 'caught'; // return이 있어도 finally 실행
} finally {
console.log('4: finally'); // 항상 실행
}
console.log('5: 실행 안 됨 (catch에서 return)');
}
example();
// 출력: "1: try" → "3: catch oops" → "4: finally"
finally는 try 또는 catch 블록에 return, break, continue, throw가 있어도 항상 실행됩니다. 자원 해제(DB 연결 닫기, 락 해제, 타이머 정리)에 적합합니다.
에러 유형에 따른 선택적 처리
try {
await fetchData(url);
} catch (e) {
if (e instanceof NetworkError) {
// 네트워크 오류 → 재시도
return retry(url);
}
if (e instanceof AuthError) {
// 인증 오류 → 로그아웃
return logout();
}
throw e; // 알 수 없는 에러는 반드시 재통
}
모든 에러를 한 번에 삼키는 것은 위험합니다. 처리할 수 있는 유형만 처리하고, 나머지는 throw e로 상위에 전파해야 합니다.
finally 안에서의 return — 주의
function tricky() {
try {
throw new Error('oops');
} finally {
return 'finally 값'; // 에러를 억제하고 이 값이 반환됨!
}
}
console.log(tricky()); // 'finally 값' — 에러가 사라짐
finally 블록에서 return하면 try/catch에서 발생한 에러나 반환값을 덮어씁니다. 이는 의도치 않은 에러 억제로 이어지므로, finally에서는 return을 쓰지 않아야 합니다.
Optional Catch Binding — ES2019
에러 변수가 필요 없을 때는 catch 다음 괄호를 생략할 수 있습니다.
function safeParseInt(str) {
try {
return parseInt(str, 10);
} catch {
// e 없어도 됨
return 0;
}
}
동기와 비동기 에러 처리의 차이
// 동기: try/catch로 포착 가능
try {
JSON.parse('invalid');
} catch (e) {
console.error(e); // SyntaxError
}
// 비동기: setTimeout 콜백의 throw는 포착 불가
try {
setTimeout(() => { throw new Error('async error'); }, 0);
} catch (e) {
// 여기서 포착되지 않음!
}
// 올바른 방법: async/await + try/catch
async function main() {
try {
await asyncOperation();
} catch (e) {
console.error(e);
}
}
setTimeout, Promise 콜백 등 비동기 컨텍스트에서 발생한 에러는 외부의 동기 try/catch로 포착할 수 없습니다.
정리
throw는Error객체를 던질 것 — 문자열/숫자는 스택 트레이스 없음- 에러는
catch를 만날 때까지 콜 스택을 거슬러 올라감 finally는 항상 실행 — 자원 해제 코드 위치finally에서return금지 — 에러가 조용히 사라짐- 처리 못 하는 에러는 반드시
throw e로 재통
지난 글: 비동기 큐와 세마포어 — 흐름 제어 패턴
다음 글: Error 객체와 스택 트레이스 — 구조와 활용
읽어주셔서 감사합니다. 😊