A catch block's error value is not typed to Error, and TypeScript will not let you assert that it is. What follows is the practical path from the error to a usable message.
const reportError = ({ message }) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({ message: error.message })
}
That compiles as JavaScript without complaint. Under TypeScript it does not:
const reportError = ({ message }: { message: string }) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({ message: error.message })
}
The offending expression is error.message. TypeScript now defaults the variable in a catch clause to unknown, which reflects reality: a thrown value carries no guarantees. The same reasoning explains why a promise's rejection type cannot be supplied through the generic parameter — Promise<ResolvedValue, NopeYouCantProvideARejectedValueType> — and why the parameter of .catch(error => {}) has to stay untyped. A throw isn't even required to carry an error:
throw 'What the!?'
throw 7
throw { wut: 'is this' }
throw null
throw new Promise(() => {})
throw undefined
Annotating the catch variable doesn't work
Since anything can be thrown, the tempting fix is to annotate the variable so onlyError is allowed through:
try {
throw new Error('Oh no!')
} catch (error: Error) {
// we'll proceed, but let's report it
reportError({ message: error.message })
}
TypeScript rejects that outright:
Catch clause variable type annotation must be 'any' or 'unknown' if specified. ts(1196)
Your code may look incapable of throwing anything else, but JavaScript permits third-party code to do exactly that — for instance by monkey-patching the error constructor to throw a different value:
Error = function () {
throw 'Flowers'
} as any
Narrowing instead of asserting
The alternative is to narrow at runtime, which keeps the compiler satisfied and covers the genuinely unexpected cases:try {
throw new Error('Oh no!')
} catch (error) {
let message = 'Unknown Error'
if (error instanceof Error) message = error.message
// we'll proceed, but let's report it
reportError({ message })
}
This can be taken one step further: when the value is not an Error, stringify it so the message still has a chance of being useful.
try {
throw new Error('Oh no!')
} catch (error) {
let message
if (error instanceof Error) message = error.message
else message = String(error)
// we'll proceed, but let's report it
reportError({ message })
}
Packaging it as a utility
The pattern is repetitive enough to live in a shared helper for everycatch block:
function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message
return String(error)
}
const reportError = ({ message }: { message: string }) => {
// send the error to our logging service...
}
try {
throw new Error('Oh no!')
} catch (error) {
// we'll proceed, but let's report it
reportError({ message: getErrorMessage(error) })
}
Two review suggestions shaped the final version: handling the case where the value isn't an actual error object, and stringifying the error object where that is possible. Combined, they produce this:
type ErrorWithMessage = {
message: string
}
function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
)
}
function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
if (isErrorWithMessage(maybeError)) return maybeError
try {
return new Error(JSON.stringify(maybeError))
} catch {
// fallback in case there's an error stringifying the maybeError
// like with circular references for example.
return new Error(String(maybeError))
}
}
function getErrorMessage(error: unknown) {
return toErrorWithMessage(error).message
}



