Right, let’s talk about error handling. You’ve probably seen (or written) code that throws a generic Error for everything. It’s the equivalent of a doctor saying “something’s wrong” and then leaving the room. Useless. In the real world, you need to know what went wrong to have any hope of fixing it or reacting appropriately. This is where type guards become your best friend, turning a chaotic catch block into a well-lit, organized diagnostic room.

The classic problem is that in a catch clause, the error is unknown. This is TypeScript’s way of forcing you to be an adult about the situation. You can’t just assume it’s an Error object. Maybe a string was thrown. Maybe null. Maybe a custom object. The universe of thrown things is vast and stupid.

The “I Have No Idea What This Is” Starter Pack

Your first line of defense is the simplest type guard: checking for properties. Let’s say you’re working with a legacy API that, in its infinite wisdom, sometimes throws HTTP status objects. You need to handle those differently from standard Error objects.

try {
  someLegacyFunction();
} catch (err: unknown) {
  // First, let's narrow it to *something* with a message.
  if (typeof err === 'object' && err !== null && 'message' in err) {
    // Now we know it's an object with a 'message' property.
    console.error('An error occurred:', err.message);

    // But wait, what if it's our special HTTP error?
    if ('statusCode' in err && typeof err.statusCode === 'number') {
      // Now we've narrowed it further! TypeScript knows `err` has `statusCode`.
      console.error(`...and it failed with HTTP ${err.statusCode}`);
      if (err.statusCode === 404) {
        handleNotFound();
        return;
      }
    }
  }
  // If we get here, it's something we didn't expect. Handle the unknown.
  console.error('An completely unhandlable mystery occurred', err);
}

This works, but it’s verbose. We’re manually narrowing the type step-by-step. It’s robust, but we can do better.

Leveling Up: The Custom Type Guard Function

The above gets messy fast. The professional move is to create a custom type guard function. This is a function that returns a type predicate (arg is SomeType). It’s a boolean-returning function where the return value is a signal to the TypeScript compiler about the type of the argument.

Let’s define a proper type for our legacy HTTP error and a guard for it.

interface HttpError extends Error {
  statusCode: number;
}

// Custom Type Guard
function isHttpError(error: unknown): error is HttpError {
  return (
    error instanceof Error && // It's at least a standard Error
    'statusCode' in error && // It has a statusCode property
    typeof (error as any).statusCode === 'number' // And that property is a number
  );
}

// A more general guard for any Error-like object
function isErrorWithMessage(error: unknown): error is { message: string } {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as any).message === 'string'
  );
}

Now our catch block becomes clean and intentional:

try {
  someLegacyFunction();
} catch (err: unknown) {
  if (isHttpError(err)) {
    // TypeScript KNOWS it's an HttpError here. Autocomplete works.
    console.error(`HTTP ${err.statusCode}: ${err.message}`);
    handleHttpError(err);
  } else if (isErrorWithMessage(err)) {
    // TypeScript knows it's some other error with a message.
    console.error('Other error:', err.message);
  } else {
    // The final frontier of nonsense.
    console.error('Something was thrown that is not an error:', err);
  }
}

See how much clearer that is? The logic reads almost like plain English. This is infinitely more maintainable.

The instanceof Trap and When to Avoid It

You might be thinking, “Why not just use err instanceof Error?” And most of the time, that’s a great first check. But it has a critical flaw: it only works for classes. If someone throws a simple object literal like { message: "oops" }, instanceof Error will return false. This is a ridiculously common pattern in older JavaScript and even some popular libraries. Relying solely on instanceof will leave you vulnerable to these perfectly valid (if annoying) thrown objects. Your type guards should be based on the structure of the object (duck typing), not its prototype chain, because the throwing ecosystem is structurally chaotic.