TypeScript에서 try-catch의 catch 변수는 unknown이며, 에러 타입을 보장할 수 없습니다. Result 패턴으로 타입 안전한 에러 핸들링 을 구현할 수 있습니다.

try-catch의 한계

TYPESCRIPT
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 키워드 같은 것이 없습니다.

커스텀 에러 클래스

TYPESCRIPT
// 기본 커스텀 에러
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 패턴

에러를 던지지 않고 반환값으로 처리하는 함수형 패턴입니다.

TYPESCRIPT
// 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 사용 예시

TYPESCRIPT
// 파싱 함수
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); // 에러 메시지
}

체이닝

TYPESCRIPT
// 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);
});

타입 안전한 에러 유니온

TYPESCRIPT
// 에러 타입을 유니온으로 명시
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로 래핑

TYPESCRIPT
// 범용 래퍼
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로 변환할 수 있다
댓글 로딩 중...