Unknown Errors in TypeScript

Learn why catch clause variables are typed as unknown instead of any, how to safely narrow them, and how to handle the different kinds of values that can be thrown at runtime.

6 min read

In TypeScript with strict mode enabled, the variable inside a catch clause is typed as unknown, not any. This means you cannot directly access properties like message or stack on a caught error without narrowing the type first.

The change reflects a fundamental truth about JavaScript: any value can be thrown. A function can throw an Error object, a string, a number, null, or even undefined. TypeScript cannot know what was thrown, so it forces you to verify the shape before you use it.

Why the Default Changed from any to unknown

Before TypeScript 4.4, catch variables were typed as any. You could write err.message without any compiler complaint, and if someone had thrown a string instead of an Error, your code would crash at runtime with no warning during compilation.

typescripttypescript
try {
  parseUserInput(rawInput);
} catch (err) {
  console.error(err.message);
}

Strict mode now enables the useUnknownInCatchVariables flag by default. With it on, the compiler reports an error on the line above because the caught value has no known properties until you check what it is.

texttext
'err' is of type 'unknown'.

This forces you to answer the question "what did I catch?" before using the value. The compiler is not being difficult. It is representing a real runtime uncertainty in the type system.

Unknown error narrowing flow

The diagram shows the decision path every catch block should take. Start by checking for an Error instance, then fall through to narrower checks. Anything unhandled becomes a generic failure.

Narrowing with instanceof

The most common and reliable way to narrow a caught value is an instanceof check against Error. This covers all standard JavaScript errors and any custom error classes that extend it.

typescripttypescript
try {
  fetchUserData(userId);
} catch (err) {
  if (err instanceof Error) {
    console.error(err.message);
  } else {
    console.error("An unknown error occurred:", err);
  }
}

Once the check passes, TypeScript knows the caught value is an Error and allows access to its message, name, stack, and cause. This is almost always the first check you want.

If your application throws custom error subclasses, define a class that extends Error and gives itself a distinct name.

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

Then chain instanceof checks from most specific to most general, so a ValidationError is caught before the broader Error fallback catches everything else that is still an Error instance.

typescripttypescript
try {
  submitForm(formData);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(`Invalid field: ${err.field}`);
  } else if (err instanceof Error) {
    console.error(err.message);
  } else {
    console.error("Unexpected error:", err);
  }
}

The order matters here. TypeScript tracks the narrowing through each branch, so checking the subclass first is what makes err.field accessible only in that branch.

Handling Non-Error Throws

Not every thrown value is an Error. In JavaScript, throwing a plain string or a number is perfectly valid. Your code needs to handle these cases gracefully instead of assuming every catch holds a proper Error object.

A practical approach is a helper that normalizes any caught value into one.

typescripttypescript
function normaliseError(thrown: unknown): Error {
  if (thrown instanceof Error) {
    return thrown;
  }
  if (typeof thrown === "string") {
    return new Error(thrown);
  }
  return new Error("An unknown error occurred");
}

Call the helper right inside the catch block, before you do anything else with the caught value, so every code path after it works with a real Error object instead of a raw unknown.

typescripttypescript
try {
  riskyOperation();
} catch (err) {
  const error = normaliseError(err);
  console.error(error.message);
}

This pattern gives you a consistent object with a message property in every case, so you always have something meaningful to log, display, or report.

Narrowing with a Type Predicate

For projects that catch errors in many places, a type predicate can make the narrowing reusable and ergonomic.

typescripttypescript
function isError(value: unknown): value is Error {
  return value instanceof Error;
}
 
try {
  processPayment(amount);
} catch (err) {
  if (isError(err)) {
    console.error(err.message);
  }
}

The value is Error return type tells TypeScript that when the function returns true, the value is an Error. This wraps the earlier check in a named, testable function.

When to Use any Instead

There are rare cases where you know exactly what is being thrown and the narrowing is unnecessary ceremony. You can annotate the catch variable explicitly.

typescripttypescript
try {
  JSON.parse(input);
} catch (err: any) {
  console.error("Invalid JSON:", err.message);
}

JSON.parse only ever throws a SyntaxError, so annotating the caught value as any is a deliberate shortcut here. Do this sparingly. Overusing it in catch blocks defeats the purpose of the safety layer.

A Complete Error Handling Pattern

Putting it together, here is a function that turns any caught value into a safe, displayable message.

typescripttypescript
function getErrorMessage(thrown: unknown): string {
  if (thrown instanceof Error) {
    return thrown.message;
  }
  if (typeof thrown === "string") {
    return thrown;
  }
  return "An unexpected error occurred";
}

Call it from any catch block that needs to log or display a failure, so the rest of the application only ever deals with a plain, readable string instead of an unverified value.

typescripttypescript
async function saveWithLogging(doc: string) {
  try {
    await saveDocument(doc);
  } catch (err) {
    console.error("Failed to save:", getErrorMessage(err));
  }
}

The function takes an unknown value and returns a string you can safely display, handling the three most likely scenarios: an Error instance, a thrown string, and anything else. You never access a property on an unverified value.

For more on structuring larger error handling strategies, see throw errors vs Result types in TypeScript and custom error classes in TypeScript.

Rune AI

Rune AI

Key Insights

  • In strict TypeScript, catch clause variables default to unknown instead of any.
  • JavaScript can throw any value, so unknown forces you to check what you caught.
  • Use instanceof Error as the first narrowing check for safe property access.
  • Create a reusable normaliseError helper to convert any thrown value into an Error.
  • Avoid annotating catch variables as any unless you have a specific reason.
RunePowered by Rune AI

Frequently Asked Questions

Why does TypeScript type catch errors as unknown?

Because JavaScript allows throwing any value, not just Error objects. You can throw a string, number, null, or even undefined. TypeScript types the catch variable as unknown to force you to check what you caught before accessing properties like message or stack.

Can I change the catch variable type back to any?

Yes, you can explicitly annotate the catch variable with : any to opt out of the unknown behavior. You can also disable useUnknownInCatchVariables in your tsconfig, but that weakens type safety across your entire project.

What is the safest way to handle an unknown error?

Use instanceof Error as the first check, which covers all built-in and custom error types. Add additional checks for the specific error subclasses your application throws. If the value is not an Error instance, convert it to one or handle it as a generic failure.

Conclusion

TypeScript typing catch variables as unknown is a safety feature, not an annoyance. It reflects the reality that JavaScript can throw any value. Narrowing with instanceof, creating a reusable error handler, and always converting unexpected throws to Error objects gives you both type safety and reliable error reporting at runtime.