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.
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.
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.
'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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
Frequently Asked Questions
Why does TypeScript type catch errors as unknown?
Can I change the catch variable type back to any?
What is the safest way to handle an unknown error?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.