C
TypeScript/Async/Lesson 06

Error Handling — err in catch is unknown; Narrow It Before Use

30 min·theory
This chapter
6/7
TypeScript

Error Handling — err in catch is unknown; Narrow It Before Use

💡 Why Does This Matter? — Catching an Error Doesn't Mean You're Done

🎯 In JS, `catch` types `err` as `any` — you can access `.message` without any checks. TS forces `unknown`, making you verify 'is this really an Error?' before proceeding.
💼 `throw` can throw anything — an Error, a string, a number, an object. Assuming a shape inside `catch` is dangerous.
Classifying errors with custom Error classes (ApiError, ValidationError, etc.) lets call sites handle each case differently.
🔗 `try/finally` guarantees resource cleanup (closing files, releasing locks) — it runs whether or not an error is thrown.
🏢 실무에서는
API call failures require different UX for 401/403/404/500: 401 → redirect to login, 403 → show permission notice, 404 → show empty state, 500 → show temporary error message. To branch on `err.status === 401` in `catch`, you need to know the type of the error object — TypeScript's `instanceof ApiError` is the safe entry point for that.

Narrowing unknown · Custom Error · try/finally

1. err in catch (err) is unknown (TS 4.4+)

ts
try { riskyOp(); }
catch (err) {
  // err is unknown — no methods or properties can be accessed
  // err.message; ❌ Object is of type 'unknown'
}

2. Three Patterns for Narrowing

ts
catch (err) {
  // (a) instanceof — class-based
  if (err instanceof Error) {
    console.log(err.message); // ✅ narrowed to Error
  }

  // (b) user-defined type guard
  if (isApiError(err)) {
    console.log(err.status); // ✅
  }

  // (c) typeof — primitive types
  if (typeof err === 'string') {
    console.log(err.toUpperCase());
  }
}

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

3. Custom Error Classes — Classify Your Errors

ts
class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError'; // for stack traces and logging
  }
}

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

throw new ApiError('Authentication failed', 401);
throw new ValidationError('Invalid email format', 'email');

At the call site:

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 — Cleanup Always Runs

ts
const conn = await db.connect();
try {
  await conn.query('SELECT ...');
} catch (err) {
  console.log('Query failed');
} finally {
  await conn.close(); // runs whether an error occurred, didn't occur, or a return was hit
}
💻 🅰️ The JS Way — Accessing .message Directly in catch
// ❌ JS — Using err without knowing what it is

async function riskyOp() {
  if (Math.random() < 0.5) throw new Error('Real error');
  else throw 'String error'; // If someone throws this
}

async function main() {
  try {
    await riskyOp();
  } catch (err) {
    console.log(err.message);  // OK if lucky, undefined if string
    console.log(err.status);   // Always undefined — unknown
  }
}
main();

// Even if trying to branch by error type
try { await riskyOp(); }
catch (err) {
  if (err.code === 'AUTH') redirectLogin();   // err.code is not guaranteed to exist
  else if (err.status === 401) redirectLogin(); // err.status is not guaranteed to exist
  else console.log(err);
}
💻 🅱️ The TS Way — Narrowing unknown + Custom Error
// ✅ TS — Custom errors + narrowing 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('Authentication failed', 401);
  if (r < 0.66) throw new ValidationError('Email format', 'email');
  throw new Error('Unknown error');
}

async function main(): Promise<void> {
  try {
    await riskyOp();
  } catch (err) {
    // err is unknown — cannot directly access .message
    if (err instanceof ApiError) {
      if (err.status === 401) console.log('To login page');
      else console.log(`API ${err.status}: ${err.message}`);
    } else if (err instanceof ValidationError) {
      console.log(`Field [${err.field}]: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('General:', err.message);
    } else {
      console.log('Unknown throw type:', err);
    }
  } finally {
    console.log('Cleanup — always runs');
  }
}
main();

// User-defined type guard pattern
function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
}
// Can be used as isApiError(err) for readability in the caller

💡 💡 Top 5 TypeScript Error Handling Essentials

1. err in catch is unknown — narrow it

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

2. Build custom Error classes and always set name

ts
class ApiError extends Error {
  constructor(msg: string, public status: number) {
    super(msg);
    this.name = 'ApiError'; // shown in logs and stack traces
  }
}

3. Use instanceof to classify errors
String comparisons like err.code === 'XYZ' risk typos and duplication. Class-based checks are safe at compile time.

4. finally always runs — use it for cleanup
Closing files, releasing database connections, releasing locks — put these in finally.

5. Never write an empty catch

ts
try { ... } catch {} // 🚨 swallowing errors — debugging hell

At minimum, log with console.error(err). Otherwise, rethrow.

⚡ Try It Yourself — Custom Error + instanceof

Throw different error types and handle them with branching logic in catch.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

What is the safest way to narrow `err` in `catch (err) { ... }` in TypeScript?
💡 Because `throw` can throw anything (an Error, a string, an object), the type of `err` is `unknown`. The safe entry point is narrowing with `instanceof Error`. Casting (`as Error`) or `any` access passes compilation but sacrifices runtime safety.
Error Handling — Narrowing unknown in catch - TypeScript