2025-09-23 20:40
-
자바스크립트 예외 처리는
try...catch...finally구문을 기본으로 하며, 예상치 못한 에러로 인해 프로그램이 중단되는 것을 방지하고 안정성을 높이는 필수적인 기법이다. -
동기 코드에서는
try...catch가 직접 동작하지만, 비동기 코드(Promise, async/await)에서는.catch()나async/await와 함께try...catch를 사용하는 특별한 방식이 필요하다. -
단순히 에러를 잡는 것을 넘어,
throw를 통해 사용자 정의 에러를 만들고, 전역 핸들러로 놓친 에러를 관리하며, 구체적인 에러 로깅과 사용자 친화적 메시지 전달을 통해 견고한 애플리케이션을 구축해야 한다.
자바스크립트 예외 처리 완벽 핸드북 에러 없는 코드를 향한 여정
프로그래밍의 세계는 예측할 수 없는 변수들로 가득하다. 완벽하게 설계된 코드라도 사용자의 잘못된 입력, 네트워크의 불안정, 예기치 않은 데이터 형식 등 수많은 이유로 에러가 발생할 수 있다. 에러는 피할 수 없는 존재다. 중요한 것은 에러가 발생했을 때 어떻게 대처하느냐이다. 애플리케이션 전체가 무너져 내리게 둘 것인가, 아니면 우아하게 상황을 수습하고 사용자에게 안정적인 경험을 제공할 것인가?
이 핸드북은 자바스크립트의 예외 처리(Exception Handling)에 대한 모든 것을 다룬다. 왜 예외 처리가 필요한지에 대한 근본적인 이야기부터 시작해, 자바스크립트 에러의 구조를 해부하고, try...catch...finally 구문의 기본 사용법, 그리고 비동기 코드에서의 예외 처리와 같은 심화 주제까지 포괄적으로 탐색한다. 이 글을 통해 당신의 코드는 한 단계 더 견고하고 신뢰성 높은 수준으로 발전할 것이다.
1. 왜 예외 처리가 필요한가? 코드의 갑옷을 입히는 이유
컴퓨터 프로그램은 정해진 명령의 순차적인 흐름이다. 만약 이 흐름 중간에 예상치 못한 문제, 즉 **에러(Error)**가 발생하면 프로그램은 다음 명령을 실행하지 못하고 그 자리에서 멈춰버린다. 이는 마치 잘 닦인 고속도로에서 갑작스러운 교통사고가 발생해 모든 차량이 멈춰서는 것과 같다.
예외 처리란 바로 이 ‘교통사고’에 대비하는 보험과 같다. 사고가 발생할 가능성이 있는 구간(try)을 미리 지정하고, 만약 사고가 발생하면(catch) 어떻게 수습할지 대안 경로를 마련해두는 것이다. 이를 통해 전체 교통 흐름(프로그램 실행)이 마비되는 최악의 상황을 막을 수 있다.
예외 처리가 없는 코드는 다음과 같은 결과를 초래한다.
-
프로그램의 갑작스러운 중단: 사용자는 아무런 안내 없이 텅 빈 화면이나 먹통이 된 인터페이스를 마주하게 된다. 이는 최악의 사용자 경험이다.
-
데이터의 손상: 중요한 데이터를 처리하는 도중 에러가 발생하면 데이터가 불완전하게 저장되거나 유실될 수 있다.
-
보안 취약점 노출: 특정 에러 정보는 시스템의 내부 구조를 노출시켜 악의적인 공격의 빌미를 제공할 수 있다.
훌륭한 프로그래머는 에러가 없는 코드를 작성하는 사람이 아니라, 에러가 발생했을 때 그것을 품위 있게 처리하는 사람이다. 예외 처리는 버그를 숨기는 것이 아니라, 예외적인 상황을 관리하여 프로그램의 안정성과 신뢰성을 확보하는 핵심적인 프로그래밍 기술이다.
2. 자바스크립트 에러의 구조 해부하기
적을 알아야 이길 수 있듯, 에러를 잘 처리하려면 먼저 자바스크립트의 에러가 어떻게 생겼는지 알아야 한다. 자바스크립트에서 에러는 단순한 메시지가 아닌, 정보를 담고 있는 **객체(Object)**이다.
모든 에러 객체의 프로토타입은 Error 객체이며, 이로부터 파생된 여러 종류의 내장 에러들이 존재한다.
| 에러 종류 | 설명 | 발생 예시 |
|---|---|---|
SyntaxError | 문법 오류. 코드를 해석(parsing)하는 단계에서 발생하며, try...catch로 잡을 수 없다. | let x =; |
ReferenceError | 참조 오류. 선언되지 않은 변수를 참조하려고 할 때 발생한다. | console.log(nonExistentVariable); |
TypeError | 타입 오류. 값이 예상된 타입이 아닐 때 발생한다. | null.f(); (null에서 속성을 읽으려 할 때) |
RangeError | 범위 오류. 값이 허용된 범위를 벗어났을 때 발생한다. | new Array(-1); (배열 길이는 음수일 수 없다) |
URIError | encodeURI(), decodeURI() 같은 함수에 잘못된 URI를 전달했을 때 발생한다. | decodeURIComponent('%'); |
EvalError | eval() 함수와 관련하여 발생하지만, 현재는 거의 사용되지 않는다. | (최신 JS 환경에서는 거의 발생하지 않음) |
이러한 에러 객체들은 공통적으로 다음과 같은 중요한 프로퍼티를 가진다.
-
name: 에러의 종류를 나타내는 문자열 (예: ‘TypeError’). -
message: 에러에 대한 설명을 담은 문자열. -
stack: 에러가 발생하기까지의 함수 호출 순서(Call Stack)를 보여주는 문자열. 디버깅 시 가장 중요한 단서가 된다.
JavaScript
try {
const user = null;
console.log(user.name); // TypeError 발생!
} catch (error) {
console.log("에러 이름:", error.name); // 출력: 에러 이름: TypeError
console.log("에러 메시지:", error.message); // 출력: 에러 메시지: Cannot read properties of null (reading 'name')
console.log("스택 정보:", error.stack); // 출력: 에러 발생 위치와 호출 스택을 상세히 보여줌
}
3. 예외 처리의 기본기 try...catch...finally
자바스크립트 예외 처리의 가장 기본적인 문법은 try...catch...finally 블록이다. 이 세 가지 키워드는 각각 명확한 역할을 수행한다.
try: 위험 지대 설정
try 블록 안에는 에러가 발생할 가능성이 있는 코드를 넣는다. 이 블록은 “일단 시도해봐”라고 말하는 것과 같다. try 블록 내의 코드가 문제없이 실행되면, catch 블록은 건너뛰고 실행이 계속된다.
catch: 비상 대책반 가동
try 블록에서 에러가 발생하는 순간, 코드의 실행은 즉시 중단되고 제어권이 catch 블록으로 넘어온다. catch 블록은 발생한 에러 객체를 인자(보통 e 또는 error로 명명)로 받아, 에러를 처리하는 로직을 수행한다.
-
에러 로깅: 에러 정보를 콘솔이나 외부 로깅 서비스에 기록하여 개발자가 문제를 파악할 수 있게 한다.
-
사용자 알림: “요청을 처리하는 중 문제가 발생했습니다.”와 같이 사용자에게 친화적인 메시지를 보여준다.
-
대체 로직 수행: 실패한 작업을 대신할 다른 코드를 실행한다.
JavaScript
try {
console.log("데이터를 서버에서 가져오기 시작...");
// 이 함수는 존재하지 않으므로 ReferenceError가 발생한다.
fetchInvalidData();
console.log("데이터 가져오기 성공!"); // 이 줄은 실행되지 않는다.
} catch (error) {
console.error("데이터를 가져오는 중 심각한 에러가 발생했습니다.");
console.error("에러 상세 정보:", error);
// 사용자에게 보여줄 UI 업데이트 로직 등
}
finally: 뒷정리는 언제나
finally 블록은 try 블록의 코드 실행이 성공하든, catch 블록에서 에러가 잡히든 상관없이 항상 실행되는 코드 블록이다. 이는 마치 영화가 해피엔딩이든 새드엔딩이든 엔딩 크레딧은 항상 올라가는 것과 같다.
finally는 주로 자원을 해제하는 데 사용된다. 예를 들어, 파일을 열어서 작업했다면 에러 발생 여부와 상관없이 파일을 닫아야 하고, 네트워크 연결을 열었다면 반드시 연결을 종료해야 한다. 이런 ‘뒷정리’ 코드를 finally에 두면 코드의 실행 흐름을 보장할 수 있다.
JavaScript
let connection;
try {
connection = openDatabaseConnection();
connection.query("SELECT * FROM users");
} catch (e) {
console.error("데이터베이스 쿼리 중 에러 발생", e);
} finally {
// 에러가 발생하든 안 하든, DB 연결은 항상 닫아주어야 한다.
if (connection) {
connection.close();
console.log("데이터베이스 연결이 안전하게 종료되었습니다.");
}
}
4. 내가 직접 에러를 만든다 throw
때로는 자바스크립트 엔진이 자동으로 발생시키는 에러만으로는 부족할 때가 있다. 애플리케이션의 특정 비즈니스 로직에 위배되는 상황을 에러로 규정하고 싶을 수 있다. 예를 들어, 사용자가 나이를 입력하는 필드에 음수를 입력하는 것은 문법적 에러는 아니지만, 논리적으로는 명백한 ‘에러’ 상황이다.
이때 throw 키워드를 사용해 직접 에러를 발생시킬 수 있다.
JavaScript
function setAge(age) {
if (typeof age !== 'number' || age < 0) {
// 논리적 에러 상황이므로 직접 에러를 던진다!
throw new Error("나이는 음수일 수 없으며 숫자여야 합니다.");
}
console.log(`나이가 ${age}세로 설정되었습니다.`);
}
try {
setAge(25); // 정상 실행
setAge(-5); // Error 발생!
} catch (error) {
console.error(error.message); // 출력: 나이는 음수일 수 없으며 숫자여야 합니다.
}
throw는 어떤 값이든 던질 수 있지만, 반드시 new Error()를 사용해 에러 객체를 던지는 것이 좋다. 그냥 문자열(throw "Invalid age")을 던지면 name, stack과 같은 유용한 정보가 포함되지 않아 디버깅이 훨씬 어려워진다.
더 나아가, Error 클래스를 상속받아 우리 애플리케이션만의 **커스텀 에러(Custom Error)**를 만들면 코드가 훨씬 명확해진다.
JavaScript
// 유효성 검사 에러를 위한 커스텀 에러 클래스
class ValidationError extends Error {
constructor(message) {
super(message); // 부모 클래스(Error)의 생성자 호출
this.name = "ValidationError";
}
}
function saveUser(user) {
if (!user.name) {
throw new ValidationError("사용자 이름은 필수입니다.");
}
// ... 저장 로직
}
try {
saveUser({ name: "" });
} catch (error) {
if (error instanceof ValidationError) {
console.log("유효성 검사 실패:", error.message);
} else {
console.log("알 수 없는 시스템 에러가 발생했습니다.");
}
}
instanceof 키워드를 사용해 에러의 종류에 따라 다른 처리를 할 수 있게 되어 훨씬 정교한 에러 핸들링이 가능해진다.
5. 심화: 비동기 코드의 예외 처리
자바스크립트의 가장 큰 특징 중 하나는 비동기(Asynchronous) 처리이다. 하지만 이 비동기 코드는 예외 처리에서 함정을 만든다. 결론부터 말하면, 전통적인 try...catch는 비동기 콜백 함수 내부의 에러를 잡지 못한다.
JavaScript
try {
setTimeout(() => {
// 이 코드는 try 블록의 실행이 끝난 후, 미래의 어느 시점에 실행된다.
// 따라서 이 에러는 바깥의 catch 블록에 잡히지 않는다!
throw new Error("비동기 에러!");
}, 1000);
} catch (e) {
// 이 코드는 절대 실행되지 않는다.
console.error("에러를 잡았다!", e);
}
try...catch는 동기적인 코드 흐름에만 유효하기 때문이다. setTimeout은 즉시 종료되고, try 블록은 성공적으로 실행된 것으로 간주된다. 1초 뒤에 콜백 함수에서 에러가 발생했을 때, catch 블록은 이미 존재하지 않는 상태다.
그렇다면 비동기 에러는 어떻게 처리해야 할까?
Promise에서의 에외 처리: .catch()
Promise는 비동기 작업의 성공 또는 실패를 나타내는 객체다. Promise 체인에서 발생하는 에러는 .then()의 두 번째 인자인 콜백 함수나, 더 권장되는 방식인 .catch() 메서드를 통해 처리한다.
.catch()는 체인 어디에서든 에러가 발생하면 그 즉시 실행되며, 그 이전의 .then()들은 모두 건너뛴다.
JavaScript
fetch('https://api.example.com/invalid-url') // 존재하지 않는 URL
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
// fetch 실패, json 파싱 실패 등 체인 내 모든 에러가 여기서 잡힌다.
console.error("API 요청 중 문제가 발생했습니다:", error);
});
async/await에서의 예외 처리: 다시 try...catch로
async/await 문법은 Promise를 기반으로 동작하지만, 비동기 코드를 마치 동기 코드처럼 보이게 만들어준다. 이 덕분에 우리는 비동기 코드에서도 친숙한 try...catch 구문을 다시 사용할 수 있게 되었다.
JavaScript
async function fetchData() {
try {
const response = await fetch('https://api.example.com/invalid-url');
// fetch가 실패하면(Promise가 reject되면) await이 에러를 throw한다.
if (!response.ok) {
// HTTP 상태 코드가 2xx가 아닌 경우도 에러로 처리
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
// 네트워크 에러, JSON 파싱 에러, 직접 throw한 에러 모두 여기서 잡힌다.
console.error("데이터를 가져오는 데 실패했습니다:", error);
}
}
fetchData();
async/await와 try...catch의 조합은 현대 자바스크립트에서 비동기 에러를 처리하는 가장 직관적이고 강력한 방법이다.
6. 전역 에러 핸들링: 최후의 보루
모든 에러를 try...catch로 감싸는 것은 현실적으로 불가능하며 바람직하지도 않다. 개발자의 실수로 미처 처리하지 못한 에러가 발생할 수 있다. 이런 ‘놓쳐버린’ 에러들을 처리하기 위한 최후의 보루가 바로 전역 에러 핸들러다.
-
브라우저 환경:
-
window.onerror: 처리되지 않은 모든 동기적 에러가 이 이벤트 핸들러로 전달된다. 에러 메시지, 파일 위치, 라인 번호 등의 정보를 받아 로깅에 활용할 수 있다. -
window.onunhandledrejection:catch로 처리되지 않은 Promise의 reject가 발생했을 때 호출된다.
-
-
Node.js 환경:
-
process.on('uncaughtException'): 처리되지 않은 예외가 발생했을 때 호출된다. -
process.on('unhandledRejection'): 처리되지 않은 Promise reject가 발생했을 때 호출된다.
-
JavaScript
// 브라우저 환경 예시
window.onerror = function(message, source, lineno, colno, error) {
console.log("처리되지 않은 전역 에러가 감지되었습니다!");
// 이 정보를 서버로 보내 로깅할 수 있다.
// return true; // 브라우저 콘솔에 에러를 표시하지 않음
};
window.onunhandledrejection = function(event) {
console.log("처리되지 않은 Promise reject가 감지되었습니다:", event.reason);
};
// 일부러 에러 발생시키기
setTimeout(() => nonExistentFunction(), 0);
Promise.reject("심각한 실패");
전역 핸들러는 에러를 로깅하고 사용자에게 마지막 알림을 보내는 용도로는 유용하지만, 전역 핸들러에 의존해 프로그램을 계속 실행시키는 것은 매우 위험하다. 처리되지 않은 예외가 발생했다는 것은 애플리케이션의 상태가 이미 비정상적일 수 있다는 신호이기 때문이다. Node.js 환경에서는 uncaughtException이 발생하면 애플리케이션을 정상적으로 종료하고 재시작하는 것이 일반적인 패턴이다.
7. 예외 처리 실전 베스트 프랙티스
-
예외를 무시하지 마라:
catch (e) {}와 같이 빈catch블록을 만드는 것은 최악의 습관이다. 에러가 발생했다는 사실 자체를 삼켜버려 디버깅을 불가능하게 만든다. 최소한console.error(e)라도 기록해야 한다. -
구체적으로 잡아라: 가능하다면
catch블록에서 에러의 종류(instanceof)를 확인하고 그에 맞는 처리를 하라. 네트워크 에러와 사용자 입력 유효성 에러는 다르게 처리해야 한다. -
에러는 던지고, 예외는 처리하라: 개발 과정에서의 버그(예: 오타로 인한
ReferenceError)는 ‘에러’이고, 예측 가능한 문제(예: 네트워크 연결 끊김)는 ‘예외’다. 예외는 복구 로직을 통해 처리하고, 명백한 버그는 빠르게 수정해야 한다. -
finally로 자원을 정리하라: 파일 핸들, 데이터베이스 연결, 구독(subscription) 등은 에러 발생 여부와 관계없이 항상 정리해야 한다.finally는 이를 위한 완벽한 장소다. -
사용자에게 친절한 메시지를 보여줘라: 사용자에게
TypeError: Cannot read properties of undefined같은 에러 메시지를 그대로 보여주지 마라. “데이터를 불러오는 데 실패했습니다. 잠시 후 다시 시도해 주세요.” 와 같이 이해하기 쉽고 실행 가능한 메시지를 제공해야 한다.
결론: 에러는 적이 아니라 신호다
예외 처리는 단순히 에러를 숨기고 프로그램의 중단을 막는 소극적인 방어 기술이 아니다. 오히려 에러라는 ‘신호’를 적극적으로 활용하여 프로그램의 안정성을 높이고, 문제의 원인을 정확히 추적하며, 사용자에게 더 나은 경험을 제공하는 능동적이고 필수적인 개발 기술이다.
try...catch의 기본부터 async/await에서의 비동기 처리, 그리고 커스텀 에러와 전역 핸들러에 이르기까지, 이 핸드북에서 다룬 내용들을 꾸준히 실천한다면 당신의 코드는 예측 불가능한 상황에서도 흔들리지 않는 견고함을 갖추게 될 것이다. 에러를 두려워하지 말고, 지혜롭게 다루는 개발자가 되길 바란다.