When every error a package throws is a plain Error with a string message, consumers are forced to parse error messages with regex or includes() checks — fragile code that breaks the moment you change wording. ISO 25010 fault-tolerance and testability both suffer: you cannot write a catch (e) { if (e instanceof NetworkError) guard without custom error types. For SDK packages that wrap external APIs, distinguishing a 429 from a 500 from a network timeout requires typed errors. Without them, consumers either swallow all errors or crash on recoverable conditions.
High because generic `Error` instances force consumers into string-parsing for error discrimination, producing fragile error handling that breaks on any message-text change.
Define and export a typed error hierarchy from src/errors.ts, then re-export it from your package entry point.
// src/errors.ts
export class SdkError extends Error {
constructor(message: string, public readonly code: string) {
super(message)
this.name = 'SdkError'
}
}
export class ApiError extends SdkError {
constructor(message: string, public readonly statusCode: number) {
super(message, 'API_ERROR')
this.name = 'ApiError'
}
}
export class RateLimitError extends ApiError {
constructor(public readonly retryAfter: number) {
super('Rate limit exceeded', 429)
this.name = 'RateLimitError'
}
}
// src/index.ts
export { SdkError, ApiError, RateLimitError } from './errors'
Consumers can then guard: if (e instanceof RateLimitError) { await sleep(e.retryAfter * 1000) }.
ID: sdk-package-quality.api-surface.error-classes
Severity: high
What to look for: Enumerate every error thrown by the SDK. For each error, search the codebase for error handling patterns:
Error (e.g., class ApiError extends Error)Error instancesstd::error::Error.Pass criteria: The package defines at least one custom error class (or typed error) that extends the language's base error type, includes useful context (error code, message, cause), and is exported from the public API. Consumers can catch specific errors: catch (e) { if (e instanceof YourApiError) { ... } } — 100% of errors thrown by the SDK must be custom Error subclasses with descriptive names. Report: "X error types found, all Y are custom error classes."
Fail criteria: The package throws only generic Error('message') or throw new Error(...) with no custom types. Consumers cannot programmatically distinguish between error types and must parse error message strings.
Skip (N/A) when: The package is a pure utility library with no async operations, no I/O, and no failure modes beyond invalid arguments (e.g., a math library, a string formatting library). If the package only validates inputs, throwing TypeError is acceptable.
Detail on fail: "All errors thrown are generic Error instances with string messages. The API client throws 'Error: Request failed' for both network errors and 4xx responses. Consumers cannot distinguish between error types without parsing the message string."
Remediation: Custom error classes let consumers handle failures programmatically. This is especially important for SDK packages that wrap APIs.
// src/errors.ts:
export class SdkError extends Error {
constructor(message: string, public readonly code: string) {
super(message)
this.name = 'SdkError'
}
}
export class ApiError extends SdkError {
constructor(
message: string,
public readonly statusCode: number,
public readonly response?: unknown
) {
super(message, 'API_ERROR')
this.name = 'ApiError'
}
}
export class RateLimitError extends ApiError {
constructor(public readonly retryAfter: number) {
super('Rate limit exceeded', 429)
this.name = 'RateLimitError'
}
}
// src/index.ts — export them:
export { SdkError, ApiError, RateLimitError } from './errors'