C
TypeScript/비동기/Lesson 06

에러 처리 — catch 의 err 는 unknown, 좁혀서 써라

30분·theory
이 챕터
6/7
TypeScript

에러 처리 — catch 의 err 는 unknown, 좁혀서 써라

💡 왜 배워야 할까요? — 잡았다고 다 끝난 게 아니다

🎯 JS 의 catch 는 err 가 any — `.message` 에 그냥 접근. TS 는 `unknown` 으로 강제해서 '진짜 Error 인지' 확인하게 합니다.
💼 throw 는 무엇이든 던질 수 있습니다 — Error, 문자열, 숫자, 객체. catch 에서 모양을 가정하면 위험.
커스텀 Error 클래스 (ApiError, ValidationError 등) 로 에러를 분류하면 호출부에서 다르게 처리할 수 있습니다.
🔗 try/finally 로 자원 정리(파일 닫기·잠금 해제) 를 보장합니다 — 에러가 나든 안 나든 실행.
🏢 실무에서는
API 호출 실패는 401/403/404/500 각각 다른 UX 가 필요합니다. 401 → 로그인 페이지, 403 → 권한 안내, 404 → 빈 상태, 500 → 일시적 오류 안내. catch 에서 `err.status === 401` 같이 분기하려면 에러 객체의 타입을 알아야 합니다 — TS 의 `instanceof ApiError` 가 그 안전한 진입점입니다.

unknown 좁히기 · 커스텀 Error · try/finally

1. catch (err) 의 err 는 unknown (TS 4.4+)

ts
try { riskyOp(); }
catch (err) {
  // err 는 unknown — 아무 메서드/속성 접근 불가
  // err.message; ❌ Object is of type 'unknown'
}

2. 좁히는 3가지 패턴

ts
catch (err) {
  // (a) instanceof — 클래스 기반
  if (err instanceof Error) {
    console.log(err.message); // ✅ Error 로 좁혀짐
  }

  // (b) 사용자 정의 타입 가드
  if (isApiError(err)) {
    console.log(err.status); // ✅
  }

  // (c) typeof — 원시 타입
  if (typeof err === 'string') {
    console.log(err.toUpperCase());
  }
}

function isApiError(e: unknown): e is ApiError {
  return e instanceof Error && 'status' in e;
}

3. 커스텀 Error 클래스 — 에러를 분류한다

ts
class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError'; // 스택 추적·로깅용
  }
}

class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

throw new ApiError('인증 실패', 401);
throw new ValidationError('이메일 형식', 'email');

호출부에서:

ts
catch (err) {
  if (err instanceof ApiError && err.status === 401) {
    redirectToLogin();
  } else if (err instanceof ValidationError) {
    showFieldError(err.field, err.message);
  } else if (err instanceof Error) {
    showGenericError(err.message);
  }
}

4. try/finally — 정리는 항상 실행

ts
const conn = await db.connect();
try {
  await conn.query('SELECT ...');
} catch (err) {
  console.log('쿼리 실패');
} finally {
  await conn.close(); // 에러 나도, 안 나도, return 으로 빠져도 실행
}
💻 🅰️ JS 방식 — catch 에서 그냥 .message 접근
// ❌ JS — err 가 무엇인지 모르는 채 사용

async function riskyOp() {
  if (Math.random() < 0.5) throw new Error('진짜 에러');
  else throw '문자열 에러'; // 누군가가 이렇게 던지면
}

async function main() {
  try {
    await riskyOp();
  } catch (err) {
    console.log(err.message);  // 운 좋으면 OK, 문자열이면 undefined
    console.log(err.status);   // 항상 undefined — 모름
  }
}
main();

// 에러 종류를 분기하려고 해도
try { await riskyOp(); }
catch (err) {
  if (err.code === 'AUTH') redirectLogin();   // err.code 가 있을지 보장 X
  else if (err.status === 401) redirectLogin(); // err.status 가 있을지 보장 X
  else console.log(err);
}
💻 🅱️ TS 방식 — unknown 좁히기 + 커스텀 Error
// ✅ TS — 커스텀 에러 + unknown 좁히기

class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

async function riskyOp(): Promise<void> {
  const r = Math.random();
  if (r < 0.33) throw new ApiError('인증 실패', 401);
  if (r < 0.66) throw new ValidationError('이메일 형식', 'email');
  throw new Error('알 수 없는 에러');
}

async function main(): Promise<void> {
  try {
    await riskyOp();
  } catch (err) {
    // err 는 unknown — 그대로 .message 접근 불가
    if (err instanceof ApiError) {
      if (err.status === 401) console.log('로그인 페이지로');
      else console.log(`API ${err.status}: ${err.message}`);
    } else if (err instanceof ValidationError) {
      console.log(`필드 [${err.field}]: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('일반:', err.message);
    } else {
      console.log('알 수 없는 형식의 throw:', err);
    }
  } finally {
    console.log('정리 작업 — 항상 실행');
  }
}
main();

// 사용자 정의 타입 가드 패턴
function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
}
// 호출부에서 가독성을 위해 isApiError(err) 처럼 쓸 수 있음

💡 💡 에러 처리 TS 핵심 5

1. catch 의 err 는 unknown — 좁혀라

ts
if (err instanceof Error) console.log(err.message);

2. 커스텀 Error 는 클래스로 만들고 name 을 박아라

ts
class ApiError extends Error {
  constructor(msg: string, public status: number) {
    super(msg);
    this.name = 'ApiError'; // 로그·스택에 표시
  }
}

3. 에러 분류는 instanceof 로
err.code === 'XYZ' 같은 문자열 비교는 오타·중복 위험. 클래스 기반이 컴파일 시점에 안전.

4. finally 는 항상 실행 — 자원 정리
파일 닫기·DB 연결 해제·잠금 해제 같은 작업은 finally 로.

5. 절대 빈 catch 쓰지 마라

ts
try { ... } catch {} // 🚨 에러 삼키기 — 디버깅 지옥

최소한 console.error(err) 라도. 아니면 다시 throw.

⚡ 직접 실행해보기 — 커스텀 Error + instanceof

에러를 종류별로 던지고 catch 에서 분기 처리합니다.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

TS 에서 `catch (err) { ... }` 의 `err` 를 가장 안전하게 좁히는 방법은?
💡 `throw` 는 무엇이든 던질 수 있으므로 (Error, 문자열, 객체) `err` 의 타입은 `unknown`. 안전한 진입점은 `instanceof Error` 로 좁히는 것. 캐스팅(`as Error`)·any 접근은 컴파일은 통과하지만 런타임 안전을 잃습니다.
에러 처리 — catch 의 unknown 좁히기 - TypeScript