
사실 에러가 발생하지 않도록 코드를 작성하는 건 거의 불가능합니다.. 그렇기에 에러는 언제 어디에서나 발생할 수 있습니다. 따라서 에러 발생을 막는 것도 중요하지만, 에러가 발생했을 때도 프로그램이 제대로 동작할 수 있도록 에러 처리를 명확하게 해주는 것이 중요합니다.
자바스크립트 프로그램을 실행하는 중, 예기치 않은 문제가 발생할 수 있습니다. 에러가 발생하면 프로그램이 강제 종료될 수 있고, 사용자는 "무슨 일이 일어난 건지" 알 수 없습니다. 또, 디버깅이 어렵고 사용자 경험도 나빠집니다. 그러나 에러 처리를 제대로 해주면, 에러가 발생해도 프로그램이 중단되지 않고 계속 실행됩니다. 나아가 사용자가 이해할 수 있는 적절한 메시지나 대체 동작을 제공할 수 있습니다. 또, 예외 상황을 제어하고 안정적인 프로그램을 만들 수 있습니다.
예외적인 상황은 직접적인 에러를 발생시키지는 않지만, 예외 상황을 방치하면 결국 에러로 이어질 수 있습니다.
// 예외 상황을 방치하면 에러로 이어질 수 있다
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
함수나 API 호출 결과가 null, undefined, -1 등 특정 값을 반환하는 경우, 이를 통해 직접 에러를 감지하고 제어하는 방식입니다. 이 방법은 명시적인 예외 처리를 피하고, 로직 흐름 안에서 자연스럽게 예외를 처리할 수 있다는 장점이 있습니다.
예시
(1) null 또는 undefined를 통한 확인
function findUser(id) {
const users = [{ id: 1, name: 'Alice' }];
return users.find(user => user.id === id); // 못 찾으면 undefined 반환
}
const user = findUser(2);
if (user) {
console.log(user.name);
} else {
console.log("사용자를 찾을 수 없습니다.");
}
(2) -1 반환 예시 (indexOf)
const fruits = ["apple", "banana", "mango"];
if (fruits.indexOf("grape") === -1) {
console.log("해당 과일은 목록에 없습니다.");
}
(3) 단축 평가 / 옵셔널 체이닝
const user = null;
console.log(user && user.name); // undefined
console.log(user?.name); // undefined (에러 없이 안전)
장점
단점
자바스크립트에서는 try...catch 문을 사용해 에러를 처리합니다.
try...catch...finally 문은 3개의 코드 블록으로 구성됩니다. finally 문은 불필요하다면 생략이 가능합니다. catch 문도 생략 가능하지만 catch 문이 없는 try 문은 의미가 없기 때문에 생략하지 않습니다.
try {
// 에러가 발생할 수 있는 코드
} catch (error) {
// 에러가 발생했을 때 실행할 코드
// error에는 try 코드 블록에서 발생한 Error 객체가 전달
} finally {
// 에러 발생 여부와 관계없이 무조건 실행 (선택)
}
try...catch...finally 문을 실행하면 먼저 try 코드 블록이 실행됩니다. 이때 try 코드 블록에 포함된 문 중에서 에러가 발생하면 발생한 에러는 catch 문의 error 변수에 전달되고 catch 코드 블록이 실행됩니다. catch 문의 error 변수는 try 코드 블록에 포함된 문 중에서 에러가 발생하면 생성되고 catch 코드 블록에서만 유효합니다. finally 코드 블록은 에러 발생과 상관없이 반드시 한 번 실행됩니다. try...catch...finally 문으로 에러를 처리하면 프로그램이 강제로 종료되지 않습니다!
try {
const result = someFunction(); // 에러 발생 가능성 있음
console.log(result);
} catch (err) {
console.error('에러 발생:', err.message); // 사용자에게 친절한 메시지 제공
} finally {
console.log('항상 실행되는 코드 (리소스 정리 등)');
}
자바스크립트는 Error 생성자 함수를 포함해 7가지의 에러 객체를 생성할 수 있는 Error 생성자 함수를 제공합니다.
자바스크립트는 에러 정보를 담기 위한 Error 객체를 제공합니다.
| 생성자 함수 | 인스턴스 |
|---|---|
| Error | 모든 에러의 기본 타입 |
| ReferenceError | 참조할 수 없는 변수 사용 시 발생 |
| TypeError | 잘못된 타입의 값 사용 시 발생 |
| SyntaxError | 문법적으로 잘못된 코드일 때 |
| RangeError | 숫자 범위가 잘못됐을 때 |
| EvalError | eval 함수 관련 에러 (거의 사용 안 됨) |
| URIError | 잘못된 URI 처리 시 |
위의 생성자 함수들이 생성한 에러 객체의 프로토타입은 모두 Error.prototype을 상속받습니다.
const error = new Error('문제가 발생했습니다!');
console.log(error.name); // "Error"
console.log(error.message); // "문제가 발생했습니다!"
console.log(error.stack); // 스택 트레이스 출력
try {
undefinedFunction();
} catch (e) {
console.error(e instanceof ReferenceError); // true
console.error(e.message); // undefinedFunction is not defined
}
단순히 Error 객체를 생성한다고 자동으로 에러가 발생하지는 않습니다.
에러를 던지는 역할은 throw 문이 합니다.
자바스크립트에서 throw 문은 개발자가 명시적으로 에러를 발생시킬 수 있도록 해주는 문법입니다.
우리는 코드에서 잘못된 상황(예: 0으로 나누기, 유효하지 않은 입력 등)을 감지했을 때, 개발자가 직접 그 상황을 예외로 다루고 싶을 수 있습니다. 이럴 때 throw를 사용하여 의도적으로 프로그램 흐름을 중단시키고, 에러 처리 로직(catch)로 제어를 넘기게 만들 수 있습니다.
**Error 객체 생성만으로는 에러가 발생하지 않는다.
**
const error = new Error('에러가 발생했습니다!');
console.log(error); // 단지 Error 객체일 뿐, 실제로 에러는 발생하지 않음
위의 예제에서 Error 생성자 함수는 단지 에러 정보를 담는 객체를 생성할 뿐이고 프로그램의 흐름에는 영향을 주지 않습니다.
👉 반드시 throw로 던져야 에러로 인식되고 중단됩니다
throw new Error('에러가 발생했습니다!'); // 여기서 진짜로 에러 발생
function divide(a, b) {
if (b === 0) {
throw new Error("0으로 나눌 수 없습니다."); // 에러 객체를 던짐
}
return a / b;
}
try {
console.log(divide(10, 0));
} catch (e) {
console.error("에러 메시지:", e.message); // "0으로 나눌 수 없습니다."
}
실행 흐름 설명
자바스크립트에서는 throw 뒤에 모든 표현식(값)이 올 수 있습니다.
throw '문자열 에러'; // 문자열도 가능 (권장하지 않음)
throw 42; // 숫자도 가능 (권장하지 않음)
throw true; // 불리언도 가능 (권장하지 않음)
throw { message: '에러' }; // 객체도 가능
하지만 일반적으로는 Error 객체 또는 그 하위 클래스를 사용합니다.
Error 객체가 제공하는 프로퍼티
| name | 에러의 이름 (기본값은 "Error") |
|---|---|
| message | 에러 메시지 |
| stack | 에러 발생 위치를 추적할 수 있는 스택 트레이스 |
많은 디버깅 도구나 로깅 시스템은 Error 객체를 기준으로 분석하므로, throw할 때 문자열이나 숫자보다 Error 객체를 사용하는 것이 유지보수에 유리합니다.
try {
throw "이건 문자열 에러입니다";
} catch (e) {
console.log(typeof e); // string
console.log(e); // 이건 문자열 에러입니다
}
try {
throw new Error("이건 Error 객체입니다");
} catch (e) {
console.log(e instanceof Error); // true
console.log(e.name); // "Error"
console.log(e.message); // "이건 Error 객체입니다"
console.log(e.stack); // 에러 발생 위치 정보 포함
}
자바스크립트에서는 Error 클래스를 상속하여 나만의 에러 클래스를 만들 수 있습니다. 이를 사용자 정의 에러라고 하며, 특정한 예외 상황을 의미 있게 분리하여 처리할 수 있도록 도와줍니다.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("유효하지 않은 이메일 형식입니다.");
}
return true;
}
try {
validateEmail("hello-world");
} catch (e) {
console.error(e.name); // ValidationError
console.error(e.message); // 유효하지 않은 이메일 형식입니다.
}
class ValidationError extends Error {
constructor(message) {
super(message); // Error 생성자에 에러 메시지 전달
this.name = "ValidationError"; // 에러 이름 명시
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("유효하지 않은 이메일 형식입니다.");
}
return true;
}
try {
validateEmail("hello-world"); // 잘못된 이메일 포맷
} catch (e) {
console.error(e.name); // "ValidationError"
console.error(e.message); // "유효하지 않은 이메일 형식입니다."
}
| 기본 Error만 사용할 경우 | 사용자 정의 Error를 사용할 경우 |
|---|---|
| 에러의 종류를 구분하기 어려움 | 에러 타입에 따라 구체적인 처리 가능 |
| 모든 에러가 "Error"로 나타남 | "ValidationError", "AuthError" 등 구체적 이름 사용 가능 |
| 협업/디버깅 시 혼란 가능 | 코드 가독성 및 유지보수 용이 |
에러는 함수 호출 스택을 따라 아래 방향으로 전파됩니다. 즉, 에러가 발생한 함수의 호출자에게 전달되며, 최종적으로 try...catch가 없는 경우 프로그램은 종료됩니다.
const foo = () => {
throw new Error("foo에서 에러 발생");
};
const bar = () => {
foo(); // 에러가 여기로 전파됨
};
const baz = () => {
bar(); // 에러가 여기로 전파됨
};
try {
baz(); // 최종 호출 지점에서 try...catch로 감싸줌
} catch (e) {
console.error("잡은 에러:", e.message);
}
에러를 어딘가에서 반드시 catch 해야 프로그램이 종료되지 않습니다.
비동기 코드(ex. setTimeout, Promise, fetch)는 일반적인 try...catch로 에러를 잡을 수 없습니다.
try {
setTimeout(() => {
throw new Error("이 에러는 못 잡음");
}, 1000);
} catch (e) {
console.error("못 잡힘:", e.message);
}
위의 throw는 비동기 콜백 내부에서 발생하기 때문에, try...catch가 감싸고 있어도 무용지물입니다.
// 1. 비동기 내부에서 처리
setTimeout(() => {
try {
throw new Error("에러 발생");
} catch (e) {
console.error("비동기 내부에서 캐치됨:", e.message);
}
}, 1000);
// 2. async/await로 처리
const asyncFunc = async () => {
try {
const res = await fetch('https://wrong.url');
const data = await res.json();
} catch (e) {
console.error('비동기 에러 캐치:', e.message);
}
};
asyncFunc();
에러는 언제, 어디서든 발생할 수 있는 것임을 항상 염두에 두어야 합니다. 코드를 작성할 때는 정상적인 흐름만을 가정하고 개발해서는 안 되며, 예기치 못한 상황이나 사용자 입력 오류, 네트워크 문제 등 다양한 예외 상황을 고려한 철저한 방어적 코딩이 필요합니다.
에러가 발생했을 때는 사용자에게 친절하고 명확한 에러 메시지를 제공함으로써, 문제의 원인을 이해하고 적절한 행동을 취할 수 있도록 안내하는 것이 중요합니다. 하지만 동시에, 개발 내부 로직이나 민감한 시스템 정보를 노출하지 않도록 주의해야 하며, 이는 보안 및 사용자 신뢰 측면에서도 매우 중요한 부분입니다!!
또 최근 웹 환경에서는 비동기 처리가 자주 사용되기 때문에 동기 흐름뿐 아니라 비동기 흐름에서 발생할 수 있는 에러까지 고려한 에러 핸들링 로직을 설계하는 습관이 필요합니다. 이를 위해서는 try...catch를 적절히 활용하고, Promise, async/await와 같은 비동기 구조에서의 에러 전파 방식과 처리 방법에 대한 정확한 이해가 바탕이 되어야 합니다.
따라서, 에러 처리는 단순히 프로그램의 예외를 막는 역할이 아니라, 사용자 경험을 지키고 시스템의 신뢰성과 안정성을 높이기 위한 핵심 개발 전략 중 하나라는 것을 기억해 주시면 좋겠습니다!