에러 핸들링 패턴 — Result 타입, 타입 안전한 예외 처리
TypeScript에서 try-catch의 catch 변수는
unknown이며, 에러 타입을 보장할 수 없습니다. Result 패턴으로 타입 안전한 에러 핸들링 을 구현할 수 있습니다.
try-catch의 한계
try {
const data = JSON.parse(input);
} catch (error) {
// error: unknown (TS 4.4+ strict 모드)
// 어떤 타입의 에러인지 알 수 없음
// ❌ 바로 접근 불가
// console.log(error.message); // Error
// ✅ 타입 좁히기 필요
if (error instanceof Error) {
console.log(error.message);
}
}
TypeScript는 함수가 어떤 에러를 던지는지 타입으로 선언할 수 없습니다. Java의 throws 키워드 같은 것이 없습니다.
커스텀 에러 클래스
// 기본 커스텀 에러
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
) {
super(message);
this.name = 'AppError';
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource}을(를) 찾을 수 없습니다`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(
message: string,
public readonly field: string,
) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
// instanceof로 타입 좁히기
function handleError(error: unknown) {
if (error instanceof ValidationError) {
console.log(`필드 ${error.field}: ${error.message}`);
} else if (error instanceof NotFoundError) {
console.log(`404: ${error.message}`);
} else if (error instanceof Error) {
console.log(`일반 에러: ${error.message}`);
} else {
console.log('알 수 없는 에러');
}
}
Result 패턴
에러를 던지지 않고 반환값으로 처리하는 함수형 패턴입니다.
// Result 타입 정의
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// 성공/실패 헬퍼 함수
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
Result 사용 예시
// 파싱 함수
function parseJSON<T>(input: string): Result<T, string> {
try {
return ok(JSON.parse(input));
} catch {
return err('유효하지 않은 JSON');
}
}
// 사용 — 에러를 강제로 처리
const result = parseJSON<{ name: string }>('{"name": "홍길동"}');
if (result.ok) {
console.log(result.value.name); // 안전하게 접근
} else {
console.log(result.error); // 에러 메시지
}
체이닝
// map: 성공 값을 변환
function mapResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => U,
): Result<U, E> {
if (result.ok) {
return ok(fn(result.value));
}
return result;
}
// flatMap: Result를 반환하는 함수와 체이닝
function flatMapResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>,
): Result<U, E> {
if (result.ok) {
return fn(result.value);
}
return result;
}
// 체이닝 예시
const result = parseJSON<{ age: string }>('{"age": "25"}');
const ageResult = flatMapResult(result, (data) => {
const age = parseInt(data.age);
if (isNaN(age)) return err('나이가 숫자가 아닙니다');
return ok(age);
});
타입 안전한 에러 유니온
// 에러 타입을 유니온으로 명시
type FetchError =
| { type: 'network'; message: string }
| { type: 'timeout'; duration: number }
| { type: 'parse'; raw: string };
async function fetchUser(
id: number
): Promise<Result<User, FetchError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err({ type: 'network', message: `HTTP ${response.status}` });
}
const text = await response.text();
try {
return ok(JSON.parse(text));
} catch {
return err({ type: 'parse', raw: text });
}
} catch {
return err({ type: 'timeout', duration: 5000 });
}
}
// 사용 — 모든 에러 경우를 처리
const result = await fetchUser(1);
if (!result.ok) {
switch (result.error.type) {
case 'network': console.log(result.error.message); break;
case 'timeout': console.log(`${result.error.duration}ms 타임아웃`); break;
case 'parse': console.log(`파싱 실패: ${result.error.raw}`); break;
}
}
try-catch를 Result로 래핑
// 범용 래퍼
function tryCatch<T>(fn: () => T): Result<T, Error> {
try {
return ok(fn());
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)));
}
}
// async 버전
async function tryCatchAsync<T>(
fn: () => Promise<T>
): Promise<Result<T, Error>> {
try {
return ok(await fn());
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)));
}
}
// 사용
const result = await tryCatchAsync(() => fetch('/api/data').then(r => r.json()));
정리
- TypeScript의 catch 변수는
unknown이므로 타입 좁히기가 필요하다 - 커스텀 에러 클래스로
instanceof기반 분기를 구현할 수 있다 - Result 패턴은 에러를 반환값으로 처리해서 타입 안전성을 보장한다
- 에러 타입을 Discriminated Union으로 정의하면 exhaustiveness check가 가능하다
tryCatch래퍼로 기존 try-catch를 Result로 변환할 수 있다
댓글 로딩 중...