TypeScript는 JavaScript의 try, catch, finally, throw를 그대로 씁니다. TypeScript만의 부분은 catch 변수입니다. strict가 켜져 있으면 타입이 unknown이므로, 사용하기 전에 무엇이 던져졌는지 확인해야 합니다.
런타임 쪽(스택이 어떻게 풀리는지, 어떤 내장 에러 타입이 있는지)은 JavaScript try/catch에서 다룹니다.
catch 변수가 unknown인 이유
JavaScript는 Error, 문자열, 숫자, undefined, 라이브러리의 객체 등 무엇이든 던질 수 있습니다. TypeScript는 그중 무엇인지 알 수 없으므로 strict(useUnknownInCatchVariables 플래그)에서는 변수가 unknown이 되고, 사용하기 전에 좁혀야 합니다.
index.ts(5,34): error TS18046: 'e' is of type 'unknown'.
strict가 없으면 e는 any이고 같은 코드가 컴파일되지만, 문자열이 던져지면 런타임에 잘못됩니다. e.message는 undefined가 되고, e.message.length처럼 더 깊이 들어가면 TypeError가 발생합니다. 타입 표기로 빠져나갈 수도 없습니다. catch (e: Error)는 TS1196 오류(Catch clause variable type annotation must be 'any' or 'unknown' if specified)입니다.
에러 좁히기
instanceof Error는 TypeError, SyntaxError, RangeError, 직접 만든 하위 클래스를 포함해 Error를 기반으로 한 모든 것을 다룹니다. 그 밖의 값은 문자열로 바꾸는 방식으로 대비합니다. 작은 헬퍼를 두면 호출하는 쪽 코드가 짧아집니다.
일부 에러만 처리하는 catch는 나머지를 다시 던져야 합니다: if (!(e instanceof ValidationError)) throw e;. 모르는 에러를 삼키면 진짜 버그가 숨겨집니다.
에러 던지기
throw는 어떤 표현식이든 받으며 TypeScript는 이를 제한하지 않습니다. 그래도 Error 객체를 던지세요. 스택 트레이스를 담고 있고, 코드베이스의 모든 instanceof Error 검사가 이에 의존합니다.
항상 예외를 던지는 함수의 반환 타입은 never이며, TypeScript는 호출 뒤의 타입 좁히기에 이를 활용합니다.
ES2022부터 Error는 cause가 담긴 두 번째 인수를 받아, 저수준 에러를 여러분이 던지는 에러에 연결합니다: throw new Error("could not load settings", { cause: e }). 호출하는 쪽은 err.cause(타입은 unknown)를 읽을 수 있습니다.
사용자 정의 에러 클래스
Error의 하위 클래스를 만들면 호출하는 쪽이 instanceof로 실패를 구별하고 추가 데이터를 담을 수 있습니다. 상속된 name은 "Error"이고 로그와 String(err)에 나타나므로 name을 설정하세요.
NotFoundError는 HttpError이기도 하므로 가장 구체적인 클래스를 먼저 검사하세요. noImplicitOverride가 켜져 있지 않다면 여기서 override 수정자는 선택 사항입니다.
오래된 가이드는 모든 에러 생성자에 Object.setPrototypeOf(this, new.target.prototype)를 추가합니다. 클래스를 ES5 함수로 컴파일할 때 Error 하위 클래스에서 instanceof가 깨졌기 때문에 필요했던 것입니다. TypeScript 7은 더 이상 target: "es5"를 지원하지 않으며(TS5108: Option 'target=ES5' has been removed를 보고합니다), ES2015 이상에서는 위처럼 네이티브 class ... extends Error가 동작합니다.
Result 패턴
TypeScript는 함수가 어떤 에러를 던질 수 있는지 추적하지 않으므로, 호출하는 쪽에 처리하라고 알려 주는 것이 없습니다. 잘못된 입력이나 없는 레코드처럼 예상되는 실패라면, "성공 또는 실패"를 말해 주는 값을 반환해서 에러를 타입 시스템 안에 넣을 수 있습니다.
r.ok를 확인하기 전에 r.value를 읽으면 컴파일 오류이며, 바로 그것이 목적입니다. 예상하지 못한 것(버그, 끊어진 연결)에는 예외를, 호출하는 쪽이 판단해야 하는 결과에는 Result 값을 쓰세요.
비동기 코드의 에러
거부된 Promise는 await 지점에서 던져진 에러가 되므로 await를 감싼 try/catch도 똑같이 동작하고, 변수는 역시 unknown입니다. 한 가지 차이는 Promise의 .catch()입니다. 그 콜백 매개변수는 unknown이 아니라 any로 타입 지정되어 있으므로 직접 지정하세요(.catch((e: unknown) => ...)). 예제는 async/await를 보세요.
흔한 실수
- 좁히지 않고
e.message를 읽음.strict에서는 컴파일되지 않고,strict가 없으면Error가 아닌 값이 던져질 때 깨집니다. - 모든 것을 잡고 그냥 계속 진행함. 예상한 에러만 처리하고 나머지는 다시 던지세요.
- 사용자 정의 에러에
name을 설정하지 않음. 그러면 로그에는 모두Error로 찍힙니다. - 문자열을 던짐.
throw "failed"는 스택 트레이스가 없고instanceof Error검사를 통과하지 못합니다.
자주 묻는 질문
TypeScript catch 블록에서 에러의 타입은 무엇인가요?
strict가 켜져 있으면(useUnknownInCatchVariables 플래그) unknown이고, 그렇지 않으면 any입니다. JavaScript는 Error 객체뿐 아니라 어떤 값이든 던질 수 있으므로 TypeScript는 확인하도록 요구합니다. e.message를 읽기 전에 if (e instanceof Error)로 좁히세요.
TypeScript에서 catch 변수의 타입을 Error로 지정할 수 있나요?
아니요. catch (e: Error)는 TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified 오류입니다. 무엇이 던져졌는지 보장할 수 없으므로 unknown과 any만 허용됩니다. 대신 블록 안에서 좁히세요.
TypeScript에서 에러는 어떻게 던지나요?
JavaScript와 같습니다: throw new Error("message"), 또는 new RangeError(...) 같은 내장 에러나 사용자 정의 하위 클래스의 인스턴스를 던집니다. TypeScript는 어떤 값이든 던지게 해 주지만, Error 객체를 던지면 스택 트레이스가 남고 instanceof Error 검사가 동작합니다.
TypeScript에서 사용자 정의 에러 클래스는 어떻게 만드나요?
Error를 상속하고 super(message, options)를 호출한 뒤 name을 설정합니다: class NotFoundError extends Error { name = "NotFoundError"; }. 추가 필드는 생성자에 둡니다. 지원되는 모든 target(ES2015 이상)에서 instanceof NotFoundError는 프로토타입 보정 없이 동작합니다.
TypeScript에 checked exception이나 throws 절이 있나요?
없습니다. 함수의 타입은 무엇을 던질 수 있는지 아무것도 말하지 않으며, 컴파일러는 에러가 처리되었는지 결코 검사하지 않습니다. 호출하는 쪽이 처리해야 하는 에러라면 Result 타입(성공과 실패의 구별된 유니언)으로 값처럼 반환해서 타입 시스템이 검사하게 하세요.