C.W.K.
Stream
Lesson 05 of 05 · published

Error Handling: `catch (e: unknown)`

~8 min · async-promises, errors, unknown, try-catch

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"JavaScript lets you throw anything. TypeScript types catches as unknown so you handle that fact."

The unknown catch parameter

JavaScript permits throw 'string', throw 42, throw { custom: 'object' }, throw new Error(...), or any other value. Your catch block must handle whatever shape arrived. With strict mode's useUnknownInCatchVariables flag (on by default in modern projects), the catch parameter's type is unknown, forcing you to narrow before accessing properties.

The pattern: catch (e) { if (e instanceof Error) console.log(e.message); else console.error('unknown error', e); }. The narrowing makes the access safe.

Why this matters more for async

Promise rejections aren't typed either. fetch().then(...).catch(e => ...)e is whatever was rejected with, which could be anything. async/await brings these into try/catch, where the unknown rule kicks in. Without strict mode, you might write e.message and silently crash on a string-throwing API.

Custom error classes for richer narrowing

When you throw, prefer extending Error: class NetworkError extends Error { constructor(public status: number) { super() } }. Then narrow with instanceof NetworkError to access the typed fields. This is the same discriminated-union pattern from earlier, applied to error hierarchies.

Treat e as untrusted input. JavaScript's flexibility means catch handlers must validate before accessing. Narrow with instanceof, branch on shape, fall back gracefully on unknown values.

Code

catch with unknown — narrow before using·typescript
// Catch with unknown — strict-mode default.
async function safeRun() {
  try {
    await riskyOp();
  } catch (e) {
    // e: unknown
    if (e instanceof Error) {
      console.error(e.message, e.stack);
    } else if (typeof e === 'string') {
      console.error('thrown string:', e);
    } else {
      console.error('thrown unknown value', e);
    }
  }
}

// Custom error class for richer narrowing.
class NetworkError extends Error {
  constructor(public readonly status: number, msg: string) {
    super(msg);
  }
}

async function fetchOrThrow(url: string) {
  const res = await fetch(url);
  if (!res.ok) throw new NetworkError(res.status, `Bad response: ${res.status}`);
  return res.json();
}

try {
  await fetchOrThrow('/users');
} catch (e) {
  if (e instanceof NetworkError) {
    console.error(`HTTP ${e.status}: ${e.message}`);
  } else {
    console.error('other error', e);
  }
}

External links

Exercise

Write a safelyRun<T>(fn: () => Promise<T>): Promise<T | null> function that runs fn and returns its result, or null on any thrown error. Make sure to handle both Error instances and other thrown values appropriately.
Hint
try { return await fn() } catch (e) { logError(e); return null }. Inside logError, narrow on instanceof Error vs unknown. The function's caller knows null means 'something went wrong' without needing to know what.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.