Chapter 9.6☕ 16 min read

Try/Catch Error Typing

catch(err) gives you unknown — like a hospital ER receiving an unknown patient.

01The Hospital Emergency Room

Error handling is one of the most important — and most misunderstood — parts of TypeScript. When you catch an error in a try/catch block, TypeScript types the error parameter as unknown. This is not a bug. It is a deliberate, carefully considered design decision that reflects the reality of JavaScript: you can throw ANYTHING.

Think of the emergency room at a hospital like Osmania General in Hyderabad. When an ambulance arrives with an unknown patient, the doctors don't start treatment immediately. First, they DIAGNOSE. They check vitals, run tests, ask questions. Only when they know what's wrong do they start treating. A patient could have a fracture, a fever, food poisoning, or a heart attack — each requires completely different treatment. Treating without diagnosing could make things worse.

That's exactly what catch(err: unknown) does. The err is an unknown patient arriving at the ER. You don't know if it's an Error object (fracture), a string (fever), a number (food poisoning), or a custom object (heart attack). You must DIAGNOSE first with instanceof, typeof, or custom checks before you can use it.

Before TypeScript 4.0, the catch parameter was typed as any, which meant you could access err.message without any check — and crash at runtime if the error was actually a string. The change to unknown forces you to write safe error handling code that checks before using. This is one of the best improvements in modern TypeScript, preventing countless runtime crashes from mis-assumed error types.

02unknown in Catch Blocks

The catch parameter type is unknown, which means you can't access any properties without narrowing. Let's explore all the ways to narrow and handle errors safely.

Method 1: instanceof Error (Most Common)

Most errors in well-written code and standard libraries are Error objects or subclasses. This is the first check you should make:

try {
  const data = JSON.parse(jsonString);
} catch (err) {
  if (err instanceof Error) {
    // err is Error — .message, .name, .stack
    console.error(err.message);
  }
}

// You can also check for specific
// Error subclasses:
if (err instanceof TypeError) {
  // Type errors
} else if (err instanceof RangeError) {
  // Range errors
} else if (err instanceof SyntaxError) {
  // JSON parse errors
}

Method 2: typeof Check (for string errors)

Some codebases throw string errors. This is bad practice, but it happens:

catch (err) {
  if (typeof err === "string") {
    console.error(err.toUpperCase());
  }
}

Method 3: Custom Error Classes

You can create your own error classes for domain-specific errors:

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

// Usage:
catch (err) {
  if (err instanceof ApiError) {
    console.error(
      `API ${err.statusCode}: ${err.message}`
    );
  }
}

Method 4: The Fallback — unknown error

When you can't determine the error type, log it safely and provide a fallback:

catch (err) {
  // Safest fallback — works for any type
  console.error(
    "An error occurred:",
    err instanceof Error
      ? err.message
      : String(err)
  );
}

The key pattern: always check instanceof Error first (most common), then specific typeof checks for primitive types, then a fallback for truly unknown error types. Never assume — always verify.

03Error Handling Patterns

Beyond basic narrowing, there are several established patterns for handling errors in TypeScript applications.

Pattern 1: The Result Type (Functional Error Handling)

Instead of throwing errors, return a discriminated union that represents success or failure:

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUser(
  id: number
): Promise<Result<User>> {
  try {
    const res =
      await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return {
        success: false,
        error: new Error(
          `HTTP ${res.status}`
        ),
      };
    }
    const data: User =
      await res.json();
    return {
      success: true,
      data,
    };
  } catch (err) {
    return {
      success: false,
      error: err instanceof Error
        ? err
        : new Error(String(err)),
    };
  }
}

// Usage:
const result = await fetchUser(1);
if (result.success) {
  console.log(result.data.name);
} else {
  console.error(result.error.message);
}

Pattern 2: tryCatch Utility

A reusable wrapper that catches errors and returns them as values:

async function tryCatch<T, E = Error>(
  fn: () => Promise<T>
): Promise<[T | null, E | null]> {
  try {
    const data = await fn();
    return [data, null];
  } catch (err) {
    return [
      null,
      err as E,
    ];
  }
}

// Usage:
const [data, error] =
  await tryCatch<User>(() =>
    fetch("/api/user").then(r =>
      r.json()
    )
  );
if (error) {
  console.error("Failed:", error);
} else {
  console.log(data.name);
}

Pattern 3: Logging with Context

Always add context when logging errors — it makes debugging exponentially easier:

catch (err) {
  console.error(
    "[fetchUser] Failed to load user",
    { userId: id, error: err }
  );
}

Pattern 4: Global Error Boundary (React/Angular)

In UI frameworks, use error boundaries to catch rendering errors and provide fallback UI. In Angular, the ErrorHandler class provides a global hook. These patterns prevent your entire app from crashing when a single component throws.

04Error Typing Traps

Error handling in TypeScript has several common pitfalls. Let's walk through the most dangerous ones.

Trap 1: Assuming err is always an Error object

This is the #1 mistake. In JavaScript, you can throw anything. Always narrow first:

try {
  throw "database timeout";
} catch (err) {
  // err is unknown — NOT Error!
  console.log(err.message);
  // 💥 Runtime crash: err.message
  // doesn't exist on string!
}

Trap 2: Swallowing errors with empty catch

try {
  await riskyOperation();
} catch (e) {
  // Silent catch — error disappears!
  // You'll never know it failed.
}

Trap 3: Using any in catch

try {
  // ...
} catch (err: any) {
  // This disables the safety!
  console.log(err.message);
  // No compile error, but might
  // crash at runtime
}

Trap 4: Not catching async errors

An error thrown inside an async function that is not caught will result in an unhandled promise rejection. Always wrap async code in try/catch, or chain .catch() on the returned promise.

Trap 5: Throwing non-Error objects

Throwing primitives (strings, numbers) is bad practice. Always throw Error objects or subclasses so consumers can reliably check with instanceof.

05Error Handling Cheatsheet

Here's your complete cheatsheet for error handling in TypeScript. Pin this to your mental board!

Catch Parameter Type:

catch (err)    // err: unknown (in strict mode)
catch (err: any) // Bypasses safety — avoid!

Error Narrowing:

instanceof Error  → .message, .name, .stack
typeof === "string"  → error message string
typeof === "number"  → error code
typeof === "object" && err !== null  → custom error
else  → fallback: String(err)

Safe Error Handler:

catch (err) {
  if (err instanceof Error) {
    console.error(err.message);
  } else if (typeof err === "string") {
    console.error(err);
  } else {
    console.error("Unknown error:", err);
  }
}

Key Rules:

  • Always narrow the error type before accessing properties
  • Check instanceof Error first — it covers standard errors
  • Never leave empty catch blocks — always log at minimum
  • Avoid using any in catch — you lose type safety
  • Always catch errors in async functions — unhandled rejections crash apps
  • Throw Error objects, not primitives — makes error handling consistent

The Golden Rule: "catch(err) is the hospital emergency room — you receive an unknown patient (err: unknown). Don't treat before diagnosing. Use instanceof and typeof checks to diagnose the error type before using any properties. A doctor who treats without diagnosis is dangerous — so is a developer who uses err.message without checking, bhai!"

Key Takeaways

  • catch(err) types the error as unknown in strict mode — you must narrow it before using
  • instanceof Error is the most common and safest way to access .message and .stack
  • JavaScript allows throwing any value — strings, numbers, objects, null, undefined
  • Never leave empty catch blocks — always log the error at minimum
  • Always catch errors in async functions to prevent unhandled promise rejections
  • Throw Error objects (or subclasses), not primitives, for consistent error handling
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase