21.7 useUnknownInCatchVariables: Safer Error Handling
Let’s talk about the try...catch statement. You’ve used it, I’ve used it. We’ve all written code that looks like this, blissfully unaware of the TypeScript sin we’re committing:
try {
riskyOperation();
} catch (error) {
// Look at me, I'm a good developer! I'm handling the error!
console.error("Something went wrong:", error.message);
// ...wait, what if `error` isn't an Error? Oops.
}
For years, the error in that catch clause was typed as any in TypeScript. It was a gaping hole in our type safety, a free pass to write error.somePropertyThatDefinitelyDoesntExist and have the compiler just shrug. It was the equivalent of the type system saying, “You’re on your own, pal. Good luck.”
The useUnknownInCatchVariables option, introduced in TypeScript 4.4, is the language’s way of finally handing you a helmet and some kneepads before you enter this particular construction site. When you enable it in your tsconfig.json, it changes the default type of the catch clause variable from the reckless any to the cautious unknown.
{
"compilerOptions": {
"strict": true,
"useUnknownInCatchVariables": true
}
}
Why unknown Over any?
This is the core of it. any is the type system’s off-switch. It says, “I opt out. Don’t check anything. Let me do what I want.” It’s the root of countless runtime errors that the compiler could have caught.
unknown, on the other hand, is the type system’s “prove it” mode. It’s the safest type possible. You can’t do anything with a value of type unknown until you prove to TypeScript what it actually is. This forces you to perform proper type narrowing, which is just a fancy term for “checking what the hell you’re dealing with before you use it.”
This is crucial because, in JavaScript, you can throw literally anything. A string, a number, an object of your own making, a custom ToastError class—the runtime doesn’t care. Assuming every thrown value is an instance of the standard Error class is a classic mistake.
How to Actually Work With an unknown Error
So your error is now unknown. The compiler won’t let you access .message. This isn’t a limitation; it’s the entire point. You are now forced to handle the ambiguity. You have two main paths:
1. The Type Guard Approach: This is the gold standard. You write a helper function that checks the shape of the caught value.
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as any).message === 'string'
);
}
try {
riskyOperation();
} catch (error) { // error is now 'unknown'
if (isErrorWithMessage(error)) {
// Now TypeScript knows `error` has a `message: string`
console.error("Caught an error:", error.message);
} else {
// Handle the case where it's something else
console.error("An unknown value was thrown:", error);
}
}
2. The Instant Cast Approach: Sometimes you’re in a hurry or you’re sure about the environment. You can cast it, but you’re surrendering the safety.
try {
riskyOperation();
} catch (error) {
// You're telling the compiler "Trust me, it's an Error"
console.error("Something went wrong:", (error as Error).message);
}
I don’t love this. It reintroduces the very risk useUnknownInCatchVariables is designed to prevent. Use it sparingly, like a fire axe behind glass.
The One Annoying Edge Case (Because Of Course)
There’s a legitimate, if slightly absurd, scenario this creates. What if the library you’re using throws a primitive, like a string? Your sophisticated type guard might fail, and you’re left with a value you can’t easily log. This is why a robust error handling function is a best practice.
function getErrorMessage(error: unknown): string {
if (isErrorWithMessage(error)) {
return error.message;
}
// Handle other potential types
if (typeof error === 'string') {
return error;
}
// Last resort: stringify it
return String(error);
}
try {
mightThrowAString();
} catch (error) {
// Safe and robust, handles anything the runtime can throw
reportError(getErrorMessage(error));
}
This option is a no-brainer. It slams shut a major door for bugs and forces you to handle errors correctly. The minor inconvenience of writing a type guard is a tiny price to pay for the massive gain in reliability. Enable it, and never look back. Your future self, debugging a production fire at 2 AM, will thank you for it.