Skip to content
Happy Programming Guide
Start learning
JavaScript

Fixing “error is of type unknown” in TypeScript catch Blocks

Since TypeScript 4.4, a caught value is unknown, not any. Why that is correct, the three ways to handle it, and a helper that ends the repetition.

People working together on laptops

'error' is of type 'unknown' appears when you write catch (error) and then use error.message. TypeScript is telling the truth: JavaScript lets you throw anything — a string, a number, undefined — so a caught value could be anything, and unknown is the type that says so. The fix is to narrow it before using it, and the cleanest way is a small helper you write once.

The error#

TYPESCRIPT
try {
  await save(data);
} catch (error) {
  console.log(error.message);      // 'error' is of type 'unknown'.
}

Before TypeScript 4.4, the caught variable was any, which allowed .message and also allowed every other mistake. The useUnknownInCatchVariables option, on by default under strict, changed it to unknown: a type you may hold but not touch until you have checked what it is.

Fix 1: narrow with instanceof#

TYPESCRIPT
catch (error) {
  if (error instanceof Error) {
    console.log(error.message);    // fine: narrowed to Error
  } else {
    console.log(String(error));    // something else was thrown
  }
}

Inside the if, TypeScript knows error is an Error and allows message, name and stack. This is the honest fix and the one to use inline.

Fix 2: a helper#

Writing the check in every catch block gets old. Put it in one function:

TYPESCRIPT
function errorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  try {
    return JSON.stringify(error);
  } catch {
    return String(error);
  }
}

try {
  await save(data);
} catch (error) {
  logger.error('Save failed: ' + errorMessage(error));
}

It handles Error objects, thrown strings, and thrown objects from libraries that do not extend Error. Most codebases end up with something like it in a utilities module.

Fix 3: a type guard for your own errors#

When you need more than the message — a status code, a field name — define the error type and a guard:

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

function isHttpError(error: unknown): error is HttpError {
  return error instanceof HttpError;
}

try {
  await fetchUser(id);
} catch (error) {
  if (isHttpError(error) && error.status === 404) {
    return null;                    // error.status is typed here
  }
  throw error;                      // rethrow anything else
}

Errors from libraries that do not subclass Error, or errors that cross a boundary where instanceof fails, need a structural check instead:

TYPESCRIPT
function hasCode(error: unknown): error is { code: string } {
  return typeof error === 'object' && error !== null && 'code' in error && typeof (error as { code: unknown }).code === 'string';
}

if (hasCode(error) && error.code === 'ENOENT') { ... }

The escape hatches#

TYPESCRIPT
catch (error: any) { ... }         // per-catch: back to no checking
catch (error) { (error as Error).message }   // assertion: trusts you, checks nothing

Both compile. Both reintroduce the runtime crash the type was protecting against: (error as Error).message on a thrown string is undefined, and the next line that expects a string fails. Use them only where you have external certainty about what can be thrown, and prefer the helper.

Turning the option off globally, "useUnknownInCatchVariables": false, is possible and not recommended; it silently weakens every catch in the project.

Promises and async#

The same applies to .catch() callbacks and to values from Promise.allSettled: the rejection reason is unknown (or any, depending on the declaration), and narrowing is the same. In an async function, try/catch around await is the idiomatic form and behaves exactly as above.

Questions people ask#

Why not just make catch variables Error?

Because the language does not guarantee it. TypeScript’s types describe what can actually happen, and anything can be thrown.

Is unknown the same as any?

No. any disables checking; unknown requires you to check before use. It is the safe counterpart.

Can I annotate the catch variable as a specific type?

Only as any or unknown. Anything more specific is disallowed, precisely because it cannot be guaranteed.

Which TypeScript version changed this?

4.4 introduced the option; it became part of strict at the same time. Every current project has it on.

Where to go next#

TypeScript vs JavaScript: which should a beginner learn?Read next

Keep reading

JavaScript

Introduction to ES6 and Beyond

The JavaScript features added since 2015 that changed how the language is written: let and const, arrow functions, destructuring, modules, async/await and…

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *