'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#
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#
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:
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:
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:
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#
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 — what the type system buys you.
- Handling fetch errors properly — the most common source of thrown values.
- Handling errors and exceptions — the language-agnostic view.