API를 호출했는데 갑자기 서버가 응답하지 않거나, 사용자가 예상치 못한 값을 입력하는 순간 — 에러는 항상 예고 없이 찾아옵니다. 잡지 않은 에러는 프로그램 전체를 멈춰버립니다.
JavaScript는 상황에 따라 다른 타입의 에러를 던집니다. 어떤 에러인지 알면 원인을 빠르게 파악할 수 있습니다.
// TypeError: 잘못된 타입 또는 null/undefined에 접근
const user = null;
user.name; // TypeError: Cannot read properties of null
// ReferenceError: 선언되지 않은 변수 참조
console.log(notDeclared); // ReferenceError: notDeclared is not defined
// SyntaxError: 문법 오류 (파싱 단계에서 발생)
eval("if ("); // SyntaxError: Unexpected end of input
// RangeError: 허용 범위를 벗어난 값
new Array(-1); // RangeError: Invalid array length
try 블록 안에서 에러가 발생하면 즉시 catch로 이동합니다. finally는 에러 발생 여부와 관계없이 항상 실행됩니다.
function readConfig(input) {
try {
const config = JSON.parse(input);
return config;
} catch (err) {
console.error("설정 파싱 실패:", err.message);
return null;
} finally {
console.log("파싱 시도 완료"); // 항상 실행
}
}
readConfig('{ "port": 3000 }'); // 성공 → finally 실행
readConfig("invalid json"); // 실패 → catch → finally 실행
finally는 파일 핸들이나 DB 연결처럼 반드시 정리해야 하는 리소스를 닫는 데 적합합니다.
throw된 에러는 콜 스택을 타고 위로 올라갑니다. 중간에 catch가 없으면 최상위까지 전파됩니다.
main()
|
loadUser()
|
fetchData() ← 여기서 에러 throw
|
(에러 전파 위로)
|
loadUser() ← catch 없음, 계속 전파
|
main() ← catch 있으면 여기서 처리
function fetchData() {
throw new Error("네트워크 오류");
}
function loadUser() {
fetchData(); // catch 없음 → 에러가 위로 전파
}
function main() {
try {
loadUser();
} catch (err) {
console.error("main에서 처리:", err.message); // "네트워크 오류"
}
}
에러 전파(Error Propagation): throw된 에러가 처리될 때까지 호출 스택을 역순으로 올라가는 동작
Error를 상속하면 도메인에 맞는 에러를 정의할 수 있습니다. name, message 외에 code 같은 필드를 추가하면 에러를 더 구체적으로 구분할 수 있습니다.
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = "AppError";
this.code = code;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource}를 찾을 수 없습니다.`, "NOT_FOUND");
this.name = "NotFoundError";
}
}
function getUser(id) {
if (!id) throw new NotFoundError("사용자");
}
try {
getUser(null);
} catch (err) {
if (err instanceof NotFoundError) {
console.error(`[${err.code}] ${err.message}`); // [NOT_FOUND] 사용자를 찾을 수 없습니다.
}
}
커스텀 에러: 내장 Error 클래스를 상속해 도메인 의미를 담은 에러 타입을 직접 정의하는 패턴
동기 코드는 try/catch로 충분합니다.
function divide(a, b) {
if (b === 0) throw new RangeError("0으로 나눌 수 없습니다.");
return a / b;
}
try {
const result = divide(10, 0);
} catch (err) {
console.error(err.message); // "0으로 나눌 수 없습니다."
}
Promise에서 발생한 에러는 .catch로 처리합니다. .catch를 붙이지 않으면 unhandledRejection 이벤트가 발생하고, Node.js에서는 프로세스가 종료될 수 있습니다.
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
}
fetchUser(1)
.then((user) => console.log(user))
.catch((err) => console.error("요청 실패:", err.message));
// .catch() 없이 rejected Promise를 방치하면:
// UnhandledPromiseRejectionWarning (Node.js)
// unhandledrejection 이벤트 (브라우저)
unhandledRejection: catch되지 않은 Promise rejection이 발생했을 때 런타임이 발생시키는 이벤트
async/await에서 발생한 에러는 동기 코드처럼 try/catch로 잡습니다. await가 rejected Promise를 반환하면 해당 위치에서 에러가 throw됩니다.
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
return user;
} catch (err) {
console.error("사용자 로딩 실패:", err.message);
throw err; // 필요하다면 상위로 재전파
}
}
async function main() {
try {
const user = await loadUser(1);
console.log(user);
} catch (err) {
console.error("main에서 최종 처리:", err.message);
}
}
catch 블록에서 throw err를 하지 않으면 에러가 그 자리에서 소멸하고, main은 에러 없이 실행됩니다. 의도적으로 에러를 삼키는 게 아니라면 재전파하거나 명확히 처리해야 합니다.
// 나쁜 예 — 에러가 무슨 이유로 발생했는지 알 수 없음
try {
doSomething();
} catch (err) {
// 아무것도 안 함
}
// 좋은 예 — 최소한 로깅
try {
doSomething();
} catch (err) {
console.error("doSomething 실패:", err);
}
// 잘못된 예 — callback 안에서 throw해도 바깥 catch에 걸리지 않음
try {
setTimeout(() => {
throw new Error("타임아웃 에러"); // 잡히지 않음
}, 1000);
} catch (err) {
console.error(err); // 실행되지 않음
}
// 올바른 예 — Promise로 감싸거나 콜백 안에서 직접 처리
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function run() {
try {
await delay(1000);
throw new Error("지연 후 에러");
} catch (err) {
console.error("잡힘:", err.message);
}
}
| 상황 | 처리 방법 | 주의 사항 |
|---|---|---|
| 동기 코드 | try / catch / finally | catch 안에서 에러를 삼키지 말 것 |
| Promise | .catch 체이닝 | .catch 누락 시 unhandledRejection 발생 |
| async/await | try / catch 감싸기 | 재전파 여부를 명시적으로 결정할 것 |
| setTimeout/콜백 | 콜백 내부에서 직접 처리 | 바깥 try/catch로는 잡히지 않음 |
에러는 막을 수 없지만, 어디서 어떻게 처리할지는 선택할 수 있습니다. 에러 전파 흐름을 이해하고 계층별로 책임을 나누면, 예외가 발생해도 무너지지 않는 코드를 작성할 수 있습니다.