Learn why TypeScript throws TS18046 when accessing properties on catch clause variables or other unknown-typed values, and how to narrow them safely.
TypeScript error TS18046 is closely related to TS2571 but was introduced in TypeScript 4.4 to provide clearer error messages specifically for catch clause variables and other contexts where a value is typed as unknown. Instead of the generic "Object is of type 'unknown'", TS18046 names the specific variable: 'e' is of type 'unknown'.
This error appears most often when strict mode is enabled, because TypeScript 4.4 added useUnknownInCatchVariables to the strict family. Before this change, catch clause variables were implicitly typed as any, which let you access .message and other properties without checking. Now TypeScript requires you to verify the type first.
This is a safety improvement. In JavaScript, you can throw anything — a string, a number, an object, or even undefined. Assuming the caught value is always an Error instance is not safe, and TS18046 protects you from that assumption.
.message on a catch clause variableThe most common trigger by far. You catch an error and immediately try to read its message property.
// ❌ Error: 'error' is of type 'unknown'. (TS18046)
async function loadUserProfile(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`)
return await response.json()
} catch (error) {
console.error(`Failed to load profile: ${error.message}`)
throw error
}
}// ✅ Narrow with instanceof before accessing properties
async function loadUserProfile(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`)
return await response.json()
} catch (error) {
if (error instanceof Error) {
console.error(`Failed to load profile: ${error.message}`)
} else {
console.error('Failed to load profile:', error)
}
throw error
}
}When wrapping errors to add context, you need to narrow before accessing the original error's properties.
// ❌ Error: 'err' is of type 'unknown'. (TS18046)
async function saveOrder(order: Order) {
try {
await db.orders.create(order)
} catch (err) {
throw new Error(`Failed to save order ${order.id}: ${err.message}`)
}
}// ✅ Build a safe error message
async function saveOrder(order: Order) {
try {
await db.orders.create(order)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
throw new Error(`Failed to save order ${order.id}: ${message}`)
}
}If you throw custom error classes with extra properties, you need to narrow to that specific class.
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public endpoint: string
) {
super(message)
this.name = 'ApiError'
}
}
// ❌ Error: 'error' is of type 'unknown'. (TS18046)
try {
await callExternalApi()
} catch (error) {
if (error.statusCode === 404) {
return null
}
throw error
}// ✅ Narrow to the specific error subclass
try {
await callExternalApi()
} catch (error) {
if (error instanceof ApiError && error.statusCode === 404) {
return null
}
throw error
}Helper functions that accept caught errors need to handle the unknown type.
// ❌ Error: 'error' is of type 'unknown'. (TS18046)
function logError(context: string, error: unknown) {
console.error(`[${context}] ${error.message}`)
console.error(error.stack)
}// ✅ Create a robust error logging utility
function logError(context: string, error: unknown) {
if (error instanceof Error) {
console.error(`[${context}] ${error.message}`)
if (error.stack) {
console.error(error.stack)
}
} else {
console.error(`[${context}] Non-error thrown:`, error)
}
}instanceof Error — this is the go-to fix for catch blocks. It narrows the variable to the Error type, giving you access to message, stack, and name:catch (error) {
if (error instanceof Error) {
console.log(error.message)
}
}function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
return 'An unknown error occurred'
}
// Usage
catch (error) {
console.error(getErrorMessage(error))
}String(error) for simple logging — if you only need a string representation and do not need specific properties:catch (error) {
console.error('Operation failed:', String(error))
}Avoid annotating catch variables as any — while catch (error: any) technically works in some configurations, it throws away type safety. Prefer narrowing instead.
Handle non-Error throws — remember that JavaScript allows throwing any value. Your catch block should handle strings, numbers, and other non-Error values gracefully.
TS18046 occurs when you try to use a value that TypeScript has typed as unknown without narrowing it first. The most common scenario is the error variable in a catch block when useUnknownInCatchVariables is enabled (which is included in strict mode since TypeScript 4.4). TypeScript types catch variables as unknown because JavaScript allows throwing any value, not just Error instances.
Use an instanceof check to narrow the error variable before accessing its properties. Write if (error instanceof Error) to safely access error.message and error.stack. For simple logging, String(error) converts any thrown value to a string. If you handle errors in many places, create a helper function like getErrorMessage(error: unknown): string to centralize the narrowing logic.
You cannot add a type annotation to catch clause variables in TypeScript. Writing catch (error: Error) is a syntax error. The catch variable is always unknown (with strict mode) or any (without it). Instead, narrow the type inside the catch block body using instanceof, typeof, or custom type guards. This is intentional — since JavaScript can throw any value, TypeScript cannot guarantee the type at the declaration site.
Get the latest TypeScript tips, tutorials, and course updates delivered straight to your inbox.