출시·고도화 중
JavaScript 안내서 · 5/6
프로그램은 언젠가 잘못된 입력, 끊긴 네트워크, 없는 파일을 만납니다. 이 장에서는 JavaScript에서 오류를 던지고 잡는 방법, 기본 오류 타입과 사용자 정의 오류, 비동기 코드의 오류 처리, 그리고 Node.js에 내장된 테스트 도구로 코드를 검증하는 방법을 다룹니다.
오류가 생기면 throw로 던지고, try...catch로 잡습니다. finally 블록은 오류가 났든 안 났든 항상 실행되므로 파일 닫기, 잠금 해제 같은 정리 작업에 씁니다. 문자열 대신 항상 Error 객체를 던지세요. 그래야 오류가 난 위치를 알려 주는 stack 정보가 함께 남습니다.
function parseAge(input) {
const age = Number(input);
if (!Number.isInteger(age) || age < 0) {
throw new RangeError(`올바른 나이가 아닙니다: ${input}`);
}
return age;
}
try {
console.log(parseAge("29")); // 29
console.log(parseAge("-3")); // 여기서 오류가 던져집니다
console.log("이 줄은 실행되지 않습니다");
} catch (error) {
console.error(error.name, error.message); // RangeError 올바른 나이가 아닙니다: -3
} finally {
console.log("검사 끝");
}JavaScript 엔진과 내장 함수는 상황에 따라 서로 다른 오류 타입을 던집니다. 모두 Error를 상속하므로 instanceof로 종류를 구분할 수 있습니다.
| 타입 | 언제 발생하나 |
|---|---|
TypeError | undefined의 속성을 읽거나 함수가 아닌 값을 호출할 때 |
ReferenceError | 선언하지 않은 변수를 쓸 때 |
RangeError | 허용 범위를 벗어난 값을 넘길 때(예: new Array(-1)) |
SyntaxError | 문법이 잘못되었거나 JSON.parse에 잘못된 문자열을 넘길 때 |
애플리케이션 고유의 오류는 Error를 상속한 클래스로 만들면 다루기 쉽습니다. 다른 오류를 감싸서 다시 던질 때는 두 번째 인수의 cause 옵션에 원래 오류를 넣으면 원인을 잃지 않습니다.
class NotFoundError extends Error {
constructor(resource, options) {
super(`${resource}을(를) 찾을 수 없습니다`, options);
this.name = "NotFoundError";
this.resource = resource;
}
}
function loadConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error("설정 파일을 읽지 못했습니다", { cause: error });
}
}
try {
throw new NotFoundError("사용자 42");
} catch (error) {
if (error instanceof NotFoundError) {
console.log("404 응답:", error.message);
} else {
throw error; // 처리할 수 없는 오류는 다시 던집니다
}
}
try {
loadConfig("{ 잘못된 JSON");
} catch (error) {
console.log(error.message, "원인:", error.cause.name); // ... 원인: SyntaxError
}모든 오류를 잡아서 조용히 삼키면 문제를 찾기 어려워집니다. 처리할 수 있는 오류만 잡고, 나머지는 다시 던지는 것이 원칙입니다.
Promise가 실패(reject)하면 try...catch는 그 Promise를 await했을 때만 오류를 잡을 수 있습니다. await 없이 호출하면 오류가 catch를 지나쳐 버립니다. 처리되지 않은 Promise 거부는 Node.js에서 기본적으로 프로세스를 종료시키므로 반드시 처리해야 합니다.
import { readFile } from "node:fs/promises";
async function readJson(path) {
try {
const text = await readFile(path, "utf8");
return JSON.parse(text);
} catch (error) {
if (error.code === "ENOENT") return null; // 파일이 없으면 null
throw error;
}
}
// then 체인에서는 .catch로 처리합니다
readJson("settings.json")
.then((data) => console.log(data ?? "설정 없음"))
.catch((error) => console.error("읽기 실패:", error));
// 여러 작업의 성공과 실패를 모두 확인하려면 allSettled
const results = await Promise.allSettled([readJson("a.json"), readJson("b.json")]);
for (const r of results) {
console.log(r.status, r.status === "fulfilled" ? r.value : r.reason.message);
}setTimeout 콜백 안에서 던진 오류는 바깥의 try...catch로 잡히지 않는다는 점도 기억하세요. 콜백은 나중에 별도로 실행되기 때문입니다. 이 동작은 6장의 이벤트 루프에서 자세히 설명합니다.
Node.js에는 별도 설치 없이 쓸 수 있는 테스트 실행기 node:test와 검증 모듈 node:assert가 들어 있습니다. 테스트 파일 이름을 *.test.js처럼 지으면 node --test가 자동으로 찾아 실행합니다.
// age.test.js
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parseAge } from "./age.js";
describe("parseAge", () => {
it("숫자 문자열을 정수로 바꿉니다", () => {
assert.equal(parseAge("29"), 29);
});
it("음수는 RangeError를 던집니다", () => {
assert.throws(() => parseAge("-1"), RangeError);
});
it("비동기 함수의 실패도 검사할 수 있습니다", async () => {
await assert.rejects(Promise.reject(new Error("실패")), /실패/);
});
it("객체는 내용을 깊게 비교합니다", () => {
assert.deepEqual({ a: [1, 2] }, { a: [1, 2] });
});
});node --test
node --test --watch더 많은 기능이 필요하면 커뮤니티 도구를 씁니다. Vitest는 Vite와 잘 맞고 실행이 빠르며, Jest는 오래 쓰여 자료가 많습니다. 두 도구 모두 describe, it(또는 test), expect를 중심으로 한 비슷한 문법을 씁니다.
// sum.test.js (Vitest)
import { describe, it, expect } from "vitest";
import sum from "./math.js";
describe("sum", () => {
it("숫자를 모두 더합니다", () => {
expect(sum(1, 2, 3)).toBe(6);
});
it("빈 인수는 0입니다", () => {
expect(sum()).toBe(0);
});
});Error 객체로 던지고, 처리할 수 있는 곳에서만 try...catch로 잡습니다.finally에, 감싼 오류의 원인은 cause에 담습니다.await와 try...catch 또는 .catch()로 반드시 처리합니다.node:test와 node:assert/strict만으로도 충분한 테스트를 쓸 수 있고, 필요하면 Vitest나 Jest를 고릅니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.