Error 객체와 스택 트레이스 — 구조와 활용
JavaScript Error 객체의 프로퍼티(name, message, stack, cause)와 스택 트레이스를 읽고 활용하는 방법, 직렬화 주의사항을 정리합니다.
지난 글에서 throw와 try/catch/finally의 동작 원리를 살펴봤습니다. 이번에는 실제로 던져지는 Error 객체의 내부 구조와 스택 트레이스를 효과적으로 활용하는 방법을 알아봅니다.
Error 객체의 표준 프로퍼티
new Error('메시지')로 생성되는 객체에는 여러 프로퍼티가 있습니다.
const e = new Error('사용자 로드 실패');
console.log(e.name); // "Error"
console.log(e.message); // "사용자 로드 실패"
console.log(e.stack); // "Error: 사용자 로드 실패\n at ..."
console.log(e.cause); // undefined (ES2022 옵션)
console.log(e.toString()); // "Error: 사용자 로드 실패"
내장 Error 서브클래스
JavaScript에는 목적에 따라 구분된 내장 Error 타입이 있습니다.
new TypeError('string expected'); // 타입 불일치
new RangeError('index out of range'); // 범위 초과
new SyntaxError('invalid syntax'); // 구문 오류
new ReferenceError('x is not defined'); // 미선언 변수
new URIError('malformed URI'); // URI 인코딩 오류
new EvalError('...'); // eval 관련 (드물게)
instanceof로 에러 유형을 구분하면 에러 처리 코드를 더 정확하게 작성할 수 있습니다.
스택 트레이스 읽기
e.stack은 에러 발생 위치부터 최초 호출 지점까지의 함수 호출 경로를 담은 문자열입니다.
Error: 사용자 로드 실패
at fetchUser (api.js:12:5)
at loadDashboard (ui.js:34:20)
at init (app.js:8:3)
at <anonymous>:1:1
- 첫 줄:
name: message - 이후 줄:
at 함수명 (파일명:줄:열)
중요: e.stack의 형식은 표준이 아닙니다. V8(Node.js, Chrome)과 SpiderMonkey(Firefox)가 다른 형식을 사용합니다. 파싱 코드를 작성할 때는 이 점을 고려해야 합니다.
JSON 직렬화 주의
const e = new Error('테스트');
console.log(JSON.stringify(e)); // "{}" — name, message, stack이 없음!
Error의 name, message, stack은 열거 불가능(non-enumerable) 프로퍼티이기 때문에 JSON.stringify가 무시합니다. 에러를 로그나 API 응답으로 보낼 때는 명시적으로 직렬화해야 합니다.
function serializeError(e) {
return {
name: e.name,
message: e.message,
stack: e.stack,
cause: e.cause ? serializeError(e.cause) : undefined,
};
}
logger.error(serializeError(err));
Error.captureStackTrace — V8 전용
Node.js(V8 엔진)에서는 커스텀 에러 클래스를 만들 때 불필요한 내부 프레임을 스택에서 제거할 수 있습니다.
class AppError extends Error {
constructor(message) {
super(message);
this.name = 'AppError';
if (Error.captureStackTrace) {
// AppError 생성자 프레임을 스택에서 제외
Error.captureStackTrace(this, AppError);
}
}
}
captureStackTrace(obj, constructorOpt)는 두 번째 인자로 전달한 함수 위의 프레임만 스택에 포함시킵니다. 결과적으로 AppError를 throw했을 때 스택의 첫 프레임이 에러 클래스 생성자가 아니라 실제 에러 발생 위치가 됩니다.
Error.prepareStackTrace — V8 구조화
V8에서는 스택 트레이스를 구조화된 객체로 받을 수 있습니다.
Error.prepareStackTrace = (err, frames) =>
frames.map(f => ({
fn: f.getFunctionName(),
file: f.getFileName(),
line: f.getLineNumber(),
col: f.getColumnNumber(),
}));
const e = new Error('test');
// e.stack은 이제 배열
console.log(e.stack[0]); // { fn: 'main', file: 'app.js', line: 5, col: 3 }
이를 활용하면 에러 위치를 소스맵과 연결하거나 로그 시스템에 구조화된 데이터를 전송할 수 있습니다.
에러 비교 — instanceof vs name
// instanceof: 프로토타입 체인 기반 (권장)
catch (e) {
if (e instanceof TypeError) { ... }
}
// name 비교: 다른 realm에서 온 에러에도 동작
catch (e) {
if (e.name === 'TypeError') { ... }
}
서로 다른 iframe이나 Node.js 모듈 간에 에러를 전달하면 instanceof가 실패할 수 있습니다. 라이브러리를 만들 때는 e.name 비교도 고려하세요.
실전 에러 로깅 패턴
class Logger {
error(message, error) {
const log = {
level: 'error',
message,
error: error instanceof Error
? serializeError(error)
: { value: String(error) },
timestamp: new Date().toISOString(),
};
// Sentry, Datadog 등으로 전송
this.#transport.send(log);
}
}
// 사용
try {
await processData(input);
} catch (e) {
logger.error('데이터 처리 실패', e);
throw e; // 재통해서 상위 에러 핸들러도 실행
}
정리
name,message,stack,cause— 에러의 네 가지 핵심 프로퍼티stack은 비표준이지만 사실상 모든 런타임이 지원JSON.stringify(error)는{}를 반환 — 항상 명시적으로 직렬화Error.captureStackTrace로 내부 프레임 제거 (V8 전용)- 에러 유형 비교:
instanceof우선, 크로스-realm은.name비교
지난 글: throw와 try/catch/finally — 에러 전파의 기초
다음 글: 커스텀 Error 클래스 — 도메인 에러 설계
읽어주셔서 감사합니다. 😊