Throwing strings, numbers, or plain objects instead of Error instances destroys the stack trace entirely — the thrown value arrives at the catch handler with no indication of where in the call stack the problem originated. ISO 25010:2011 §6.5.4 classifies this as a reliability defect. Beyond diagnostics, plain-object throws break instanceof checks that callers use to distinguish error types: a catch handler checking if (err instanceof NotFoundError) silently falls through when the throw was { code: 404, text: '...' }.
Low because non-Error throws do not prevent the exception from being caught, but they destroy stack traces and break instanceof-based error discrimination, making errors significantly harder to diagnose and handle correctly.
Always throw Error instances. For typed error handling, define custom classes that extend Error and set the prototype correctly for correct instanceof behavior after TypeScript compilation:
export class UserNotFoundError extends Error {
constructor(public readonly userId: string) {
super(`User not found: ${userId}`)
this.name = 'UserNotFoundError'
// Required for instanceof to work correctly when compiled to ES5:
Object.setPrototypeOf(this, new.target.prototype)
}
}
// Throw:
throw new UserNotFoundError(id)
// Catch with discrimination:
if (error instanceof UserNotFoundError) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
ID: code-quality-essentials.organization.custom-error-classes
Severity: low
What to look for: Search source files for throw statements. Check what is being thrown: throw new Error(...) and throw new CustomError(...) are correct. Look for anti-patterns: throw 'some string', throw { message: '...' }, throw { code: 404, text: 'Not found' }. Plain objects and strings thrown as errors lose the stack trace entirely and are not catchable with instanceof checks. Also check custom error class definitions — they should extend Error and call super(message) in the constructor. In TypeScript, custom errors also need Object.setPrototypeOf(this, new.target.prototype) for correct instanceof behavior when transpiled.
Pass criteria: Enumerate all relevant code locations. All throw statements throw Error instances or classes that extend Error. Custom error classes call super(message) and set the prototype correctly with at least 1 verified location.
Fail criteria: Strings, plain objects, or numbers are thrown instead of Error instances.
Skip (N/A) when: No custom error handling code exists in the project (no throw statements beyond those in third-party code).
Detail on fail: "Code throws raw strings or plain objects instead of Error instances; stack traces are lost and instanceof checks cannot be used"
Remediation: Always throw Error instances. For custom error types:
// Bad: throws string (no stack trace, no instanceof check)
throw 'User not found'
// Bad: throws plain object (no prototype chain, no stack trace)
throw { code: 404, message: 'Not found' }
// Good: custom error class with full stack trace and instanceof support
export class UserNotFoundError extends Error {
constructor(public readonly userId: string) {
super(`User not found: ${userId}`)
this.name = 'UserNotFoundError'
// Required for correct instanceof behavior when compiled to ES5:
Object.setPrototypeOf(this, new.target.prototype)
}
}
// Usage:
throw new UserNotFoundError(id)
// Catch:
if (error instanceof UserNotFoundError) {
return Response.json({ error: 'User not found' }, { status: 404 })
}