TYPESCRIPT · 심층 가이드
TypeScript 에러 처리 완전 정리
TypeScript는 던져지는 값의 타입을 검사해 주지 않기 때문에, catch로 잡은 값이 정말 Error라는 보장은 언어가 아니라 코드가 직접 만들어야 합니다.
다른 정적 타입 언어와 달리 TypeScript의 시그니처에는 어떤 오류가 나갈 수 있는지가 전혀 적히지 않습니다. throw는 문자열이든 객체든 무엇이든 던질 수 있고, 그래서 catch의 변수 타입도 unknown입니다. 여기서 초심자는 곧장 err.message를 읽으려다 막힙니다. 오류를 값으로 다룰지 예외로 던질지, 그리고 어느 경계에서 정체를 확인할지를 매번 스스로 정해야 하는 언어입니다.
try catch 기본에서 잡은 값의 정체를 좁히는 법을 익히고 나면, 사용자 정의 오류로 도메인마다 구분 가능한 타입을 세우는 이유가 드러납니다. 도메인 오류 표현은 그 클래스들을 판별 가능한 형태로 묶는 단계이고, Result 반환은 아예 예외를 쓰지 않고 성공과 실패를 같은 반환값 안에 담는 반대편 접근입니다. 마지막으로 입력 가드와 다단계 검증은 그 경계를 함수의 어느 지점에 둘지를 손으로 정해 보는 실습에 해당합니다.
finally 블록에서 return하면 진행 중이던 예외가 조용히 사라집니다. 정리 코드가 값을 돌려주는 순간 원래의 실패가 성공으로 뒤바뀌므로, finally에는 자원 해제만 두고 반환도 재던지기도 넣지 않는 편이 안전합니다. 잡은 값을 좁힐 때도 instanceof 하나만 믿기보다 필드 구조를 확인하는 가드를 함께 두면, 번들 경계나 워커를 넘나들며 프로토타입 사슬이 달라진 오류에서도 판정이 어긋나지 않습니다.
01try catch 기본
일부러 깨뜨린 JSON 문자열을 JSON.parse에 넘겨 던져진 오류를 잡고, instanceof Error로 확인한 뒤 메시지를 출력합니다. 외부 문자열을 파싱하는 지점마다 이 형태가 필요합니다. 최신 설정에서는 error가 unknown이라 이 좁히기 없이는 message를 읽을 수 없습니다.
TypeScript code
// try catch 기본
try {
JSON.parse("{");
} catch (error) {
console.log(error instanceof Error ? error.message : String(error));
}JSON.parse가 던지는 값은 SyntaxError이고 메시지에 실패한 위치가 담기므로, 로그에 그대로 남기면 어느 문자에서 깨졌는지 알 수 있습니다.
try 블록을 넓게 잡으면 파싱 외의 오류까지 같은 catch에 잡혀, 관계없는 버그가 파싱 실패로 보고됩니다.
02사용자 정의 오류
requireName은 trim()으로 공백뿐인 이름을 걸러 내고, 비었으면 Error를 던집니다. 잘못된 값이 더 깊이 흘러가기 전에 경계에서 막는 방식입니다. 오류 종류를 구분하려면 Error를 상속한 클래스를 만들고 name을 지정해 instanceof로 판별합니다.
TypeScript code
// 사용자 정의 오류
function requireName(name: string) {
if (!name.trim()) throw new Error("name required");
return name;
}
console.log(requireName("Ada"));Error를 상속한 클래스에서는 생성자에서 this.name을 지정해 두어야 스택 첫 줄과 로그에 클래스 이름이 드러납니다.
컴파일 타깃이 ES5이면 내장 Error 상속에서 프로토타입이 끊겨 instanceof가 거짓이 되므로, 타깃을 ES2015 이상으로 올려야 합니다.
03입력 검증
parseNumber는 문자열을 Number()로 변환하고 Number.isNaN이면 null을 돌려줍니다. 반환 타입이 number | null로 추론되어 호출부가 실패를 무시하기 어렵습니다. 다만 빈 문자열은 0으로 변환되어 이 검사를 통과하므로 앞에서 따로 걸러야 합니다.
TypeScript code
// 입력 검증
const parseNumber = (value: string) => {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("3"));Number.isNaN은 전역 isNaN과 달리 인자를 숫자로 변환하지 않아, 문자열이 들어와도 참을 돌려주는 일이 없습니다.
Number('')와 공백뿐인 문자열은 0으로 변환돼 이 검사를 통과하므로, 비어 있는 입력이 유효한 0으로 저장됩니다.
04안전한 숫자 파싱
assert는 조건이 거짓일 때만 메시지를 담아 던지고, 호출부는 참을 넘겨 그냥 통과합니다. 안전한 파싱보다는 전제 조건 확인에 가깝습니다. condition에 타입이 없어 암묵적 any이며, asserts condition 시그니처를 붙여야 호출 이후 타입이 좁혀집니다.
TypeScript code
// 안전한 숫자 파싱
function assert(condition, message: string) {
if (!condition) throw new Error(message);
}
assert(true, "must be true");function assert(c: unknown, m: string): asserts c로 선언하면 호출 이후 구간에서 값이 참으로 좁혀져 추가 검사가 필요 없어집니다.
condition에 타입을 적지 않으면 암묵적 any가 되고, noImplicitAny를 켠 프로젝트에서는 이 파일부터 컴파일이 멈춥니다.
05안전한 나눗셈
JSON.parse가 실패하도록 만들어 catch가 어떻게 동작하는지 보여 줍니다. 나눗셈은 사정이 달라서, 자바스크립트에서 1 / 0은 예외 대신 Infinity를, 0 / 0은 NaN을 냅니다. 따라서 이런 try가 아니라 분모를 미리 확인하는 검사가 필요합니다.
TypeScript code
// 안전한 나눗셈
try {
JSON.parse("{");
} catch (error) {
console.log(error instanceof Error ? error.message : String(error));
}결과가 정상 범위인지 보려면 Number.isFinite를 쓰면 되고, 이 함수 하나로 Infinity와 NaN을 함께 걸러 낼 수 있습니다.
0으로 나눠 나온 Infinity는 예외가 없어 그대로 흘러가고, 나중에 JSON.stringify에서 null로 바뀌며 값이 소리 없이 사라집니다.
06finally 정리
이름이 비었는지 확인하고 아니면 그대로 돌려주는 함수이며, 정리 구문은 아직 없습니다. 파일 핸들이나 잠금처럼 반드시 놓아야 하는 자원이 있다면 finally에서 반환합니다. finally 안에서 값을 return하면 앞서 던진 오류가 삼켜지므로 피해야 합니다.
TypeScript code
// finally 정리
function requireName(name: string) {
if (!name.trim()) throw new Error("name required");
return name;
}
console.log(requireName("Ada"));finally는 try 안에서 return한 뒤에도 실행되므로, 자원 반납처럼 어떤 경로로 나가든 해야 하는 일에 적합합니다.
finally 안에서 return이나 throw를 쓰면 원래 던져지던 오류가 덮여 사라져, 실패 원인이 로그에 남지 않습니다.
07오류 다시 던지기
이 함수는 던지는 대신 실패를 null로 표현합니다. 다시 던지기는 오류를 기록만 하고 처리는 상위에 맡길 때 필요하며, catch 블록 끝에서 같은 값을 throw하면 됩니다. 새 Error로 감쌀 때 cause를 지정하면 원래 오류를 잃지 않습니다.
TypeScript code
// 오류 다시 던지기
const parseNumber = (value: string) => {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("7"));new Error('msg', { cause: error })로 감싸면 원인 체인이 유지되어, 상위에서 error.cause로 최초 실패를 그대로 볼 수 있습니다.
catch에서 throw error.message처럼 문자열만 던지면 스택이 사라져, 어느 줄에서 실패했는지 추적할 수 없게 됩니다.
08도메인 오류 표현
조건과 메시지를 받아 위반했을 때만 던지는 최소 검사기입니다. 도메인 규칙마다 문구를 다르게 넣어 어디서 어긋났는지 남길 수 있습니다. 다만 문자열 메시지로만 구분하면 호출부에서 분기하기 어려워, 코드 필드나 별도 클래스를 가진 오류 타입으로 나누는 편이 낫습니다.
TypeScript code
// 도메인 오류 표현
function assert(condition, message: string) {
if (!condition) throw new Error(message);
}
assert(true, "must be true");오류에 code 필드를 두고 리터럴 유니온으로 좁히면, 호출부의 switch에서 처리하지 않은 종류를 컴파일 단계에서 잡을 수 있습니다.
메시지 문자열을 비교해 분기하면 문구를 다듬는 순간 조건이 조용히 깨지고, 문구를 다국어로 바꿀 때 전부 무력해집니다.
09fallback 값 사용
파싱이 실패하면 메시지를 출력하는 데서 끝나는데, 대체값을 쓰려면 catch 안에서 기본 객체를 돌려주도록 바꿉니다. 설정 파일이나 캐시를 읽을 때 흔한 형태입니다. JSON.parse의 반환 타입은 any라, 결과를 검증 없이 쓰면 타입 검사가 사실상 무력해집니다.
TypeScript code
// fallback 값 사용
try {
JSON.parse("{");
} catch (error) {
console.log(error instanceof Error ? error.message : String(error));
}설정 로딩처럼 실패해도 진행해야 하는 자리에서는 기본값을 돌려주되, 어떤 이유로 기본값을 썼는지 한 줄이라도 로그로 남겨야 합니다.
JSON.parse의 반환은 any라 기본값과 합쳐 그대로 쓰면, 필드가 빠지거나 타입이 달라도 검사되지 않고 그대로 흘러갑니다.
10assertion
requireName이 조건을 확인하고 어기면 즉시 중단하므로, 반환된 값은 항상 내용이 있는 문자열입니다. 내부 불변식을 지켜야 하는 지점에 둡니다. 그래도 타입은 여전히 string이라, 검증을 거쳤다는 사실까지 타입에 담으려면 브랜드 타입이나 타입 서술어가 필요합니다.
TypeScript code
// assertion
function requireName(name: string) {
if (!name.trim()) throw new Error("name required");
return name;
}
console.log(requireName("Ada"));검증을 통과한 값을 string & { __brand: 'Name' } 같은 브랜드 타입으로 표시하면, 검증하지 않은 문자열이 같은 자리에 들어오는 것을 막을 수 있습니다.
타입은 컴파일 후 사라지므로 name: string이라고 적어 두어도 외부 JSON에서 숫자가 들어오면 trim() 호출에서 런타임 오류가 납니다.
11Result 반환
성공하면 숫자를, 실패하면 null을 돌려주어 예외 없이 결과를 표현합니다. 실패 이유까지 전하려면 { ok: true, value }와 { ok: false, error }를 묶은 판별 유니온으로 확장합니다. ok를 확인하는 순간 타입이 좁혀져 잘못된 필드 접근이 막힙니다.
TypeScript code
// Result 반환
const parseNumber = (value: string) => {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("11"));{ ok: true; value: T }와 { ok: false; error: E }를 묶으면 ok 확인 순간 나머지 필드가 좁혀져, 실패 쪽에서 값을 읽는 실수가 막힙니다.
실패를 null로만 표현하면 값이 정상적으로 null인 경우와 구분되지 않아, 두 상황이 섞인 뒤에는 원인을 되짚기 어렵습니다.
12다단계 검증
assert 하나로 단계마다 조건을 확인하는 구조이고, 예시는 통과하는 경우만 보여 줍니다. 여러 규칙을 이어 붙이면 첫 위반에서 곧바로 멈춥니다. 폼처럼 모든 오류를 한 번에 보여 줘야 하는 화면에서는 던지는 대신 위반 목록을 모으는 방식이 맞습니다.
TypeScript code
// 다단계 검증
function assert(condition, message: string) {
if (!condition) throw new Error(message);
}
assert(true, "must be true");여러 규칙을 모두 보여 줘야 한다면 검사를 배열로 만들고 위반 항목만 모아 돌려주는 편이, 던지고 잡는 구조보다 화면에 붙이기 쉽습니다.
첫 위반에서 곧바로 던지면 사용자가 항목을 하나 고칠 때마다 다음 오류가 새로 나타나, 폼을 여러 번 제출하게 만듭니다.
13로깅 래퍼
catch가 잡은 값이 Error인지 확인한 뒤 메시지만 뽑아 기록합니다. 로깅 래퍼의 뼈대가 바로 이 변환 부분입니다. 자바스크립트는 문자열이나 숫자 등 무엇이든 던질 수 있어서, String(error)로 넘어가는 대비가 실제로 쓸모 있습니다.
TypeScript code
// 로깅 래퍼
try {
JSON.parse("{");
} catch (error) {
console.log(error instanceof Error ? error.message : String(error));
}기록에는 console.log 대신 console.error를 쓰면 출력이 표준 오류로 나가, 셸에서 정상 출력과 분리해 다른 곳으로 보낼 수 있습니다.
메시지만 남기고 error.stack을 버리면 같은 문구를 내는 여러 지점 중 어디서 났는지 알 수 없어, 재현 없이는 조사할 수 없습니다.
14recover 흐름
값이 규칙에 맞지 않으면 던지고, 맞으면 그대로 흘려보냅니다. 복구 흐름은 이 호출을 감싸는 쪽에서 오류를 잡아 기본 이름으로 대체하는 형태가 됩니다. 어디서 복구할지 정하지 않고 모든 층에서 잡으면, 문제가 조용히 감춰져 원인을 찾기 어려워집니다.
TypeScript code
// recover 흐름
function requireName(name: string) {
if (!name.trim()) throw new Error("name required");
return name;
}
console.log(requireName("Ada"));복구 지점은 요청 처리기나 작업 루프처럼 대안을 실제로 고를 수 있는 층에 두고, 그 아래 함수들은 오류를 그대로 통과시키는 편이 흐름이 단순합니다.
모든 층에서 오류를 잡아 기본값을 돌려주면 실패가 정상 결과로 위장되어, 잘못된 데이터가 저장된 뒤에야 문제가 드러납니다.
15입력 가드
문자열을 숫자로 바꾸고 변환 실패를 null로 걸러 내는 가드 함수입니다. 쿼리 문자열이나 폼 값처럼 바깥에서 온 문자열이 들어오는 입구에 둡니다. 타입은 컴파일 뒤 사라지므로 응답을 number로 선언했다고 해서 실제 값이 숫자라는 보장은 없습니다.
TypeScript code
// 입력 가드
const parseNumber = (value: string) => {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("15"));값이 원하는 모양인지 확인하는 함수를 value is Query 타입 서술어로 선언하면, 통과한 뒤 구간에서 타입이 좁혀진 채로 이어집니다.
쿼리 문자열 값은 배열로 들어올 수도 있어, 문자열이라고만 가정하면 같은 파라미터가 두 번 왔을 때 변환 결과가 NaN이 됩니다.
정리하며
catch의 값은unknown이므로message를 읽기 전에 좁힙니다.finally에서return하면 예외가 삼켜지니 정리 코드만 둡니다.- 복구 가능한 실패는 예외 대신
Result로, 버그성 상태는 즉시 던져 구분합니다. - 사용자 정의 오류에 코드 필드를 두어 메시지 문자열 비교에 의존하지 않게 합니다.
더 깊이 들어가고 싶다면 TypeScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.