@shirudo/base-error
Version:
A simple error base class for TypeScript
466 lines (370 loc) • 15.5 kB
Markdown
# @shirudo/base-error
[](LICENSE)
[](https://www.npmjs.com/package/@shirudo/base-error)
[](https://bundlephobia.com/package/@shirudo/base-error)
[](https://github.com/shi-rudo/base-error-ts/actions/workflows/tests.yml)
A robust, cross-environment base error class for TypeScript applications that works seamlessly across Node.js, modern browsers, and edge runtimes (like Cloudflare Workers, Deno Deploy, and Vercel Edge Functions).
## Features
- 🌐 **Cross-platform compatibility**: Works in Node.js, browsers, and edge runtimes
- 🔍 **Rich stack traces**: Captures the best possible stack trace for the current environment
- 🔄 **Error cause chain**: Preserves the error cause chain, even in environments without native support
- ⏱️ **Built-in timestamps**: Includes both numeric (epoch) and ISO string timestamps
- 🧬 **Proper inheritance**: Maintains prototype chain for reliable `instanceof` checks
- 📊 **JSON serialization**: Built-in `toJSON` method for easy logging
- ✨ **Automatic name inference**: No need to specify the error name twice (v2.0+)
- 👤 **User-friendly messages**: Built-in support for user-friendly error messages and internationalization with preserved insertion order
## Installation
```bash
npm install @shirudo/base-error
```
> **📖 Upgrading from v2?** See the [Migration Guide](MIGRATION-v2-to-v3.md) for detailed upgrade instructions.
## Usage
### Basic Usage
```typescript
import { BaseError } from "@shirudo/base-error";
// Create a custom error class using automatic name inference
class UserNotFoundError extends BaseError<"UserNotFoundError"> {
constructor(userId: string) {
super(`User ${userId} not found`);
// Optional: Add user-friendly message for end users
this.withUserMessage("The requested user could not be found.");
}
}
// Throw the error
throw new UserNotFoundError("user-123");
```
### With Error Cause
```typescript
import { BaseError } from "@shirudo/base-error";
class DatabaseError extends BaseError<"DatabaseError"> {
constructor(message: string, cause?: unknown) {
super(message, cause);
}
}
class UserServiceError extends BaseError<"UserServiceError"> {
constructor(message: string, cause?: unknown) {
super(message, cause);
}
}
try {
// Some database operation that fails
throw new Error("Connection refused");
} catch (dbError) {
// Wrap the low-level error with more context
throw new UserServiceError("Failed to fetch user data", dbError);
}
```
### Automatic Name Inference
BaseError automatically infers the error name from the class name, eliminating the need to specify it twice:
```typescript
import { BaseError } from "@shirudo/base-error";
class UserNotFoundError extends BaseError<"UserNotFoundError"> {
constructor(userId: string) {
super(`User ${userId} not found`); // Name is automatically inferred
}
}
class ValidationError extends BaseError<"ValidationError"> {
constructor(field: string, message: string, cause?: unknown) {
super(`Validation failed for ${field}: ${message}`, cause);
}
}
```
### JSON Serialization
```typescript
import { BaseError } from "@shirudo/base-error";
class ApiError extends BaseError<"ApiError"> {
constructor(statusCode: number, message: string, cause?: unknown) {
super(message, cause); // Using automatic name inference
this.statusCode = statusCode;
}
statusCode: number;
// Override toJSON to include custom properties
toJSON() {
const json = super.toJSON();
return {
...json,
statusCode: this.statusCode,
};
}
}
const error = new ApiError(404, "Resource not found");
console.log(JSON.stringify(error, null, 2));
```
### User-Friendly Messages (v2.1+)
BaseError supports user-friendly messages that can be localized for different languages, making it perfect for applications that need to show end-user error messages:
```typescript
import { BaseError } from "@shirudo/base-error";
class UserNotFoundError extends BaseError<"UserNotFoundError"> {
constructor(userId: string) {
super(`User with id ${userId} not found in database lookup`); // Technical message
// Set user-friendly message
this.withUserMessage(`User ${userId} was not found.`);
// Add localized messages
this.addLocalizedMessage("en", "User not found. Please check the user ID and try again.")
.addLocalizedMessage("es", "Usuario no encontrado. Verifique el ID de usuario e inténtelo de nuevo.")
.addLocalizedMessage("fr", "Utilisateur introuvable. Veuillez vérifier l'ID utilisateur et réessayer.")
.addLocalizedMessage("de", "Benutzer nicht gefunden. Bitte überprüfen Sie die Benutzer-ID und versuchen Sie es erneut.");
}
}
// Usage in error handling
try {
throw new UserNotFoundError("user-123");
} catch (error) {
if (error instanceof UserNotFoundError) {
// Get localized message based on user preference
const userMessage = error.getUserMessage({
preferredLang: "es",
fallbackLang: "en"
});
console.log("User message:", userMessage); // "Usuario no encontrado..."
// Technical message for logging
console.log("Technical message:", error.message); // "User with id user-123 not found..."
}
}
```
#### User Message API
The user message functionality provides four methods:
1. **`withUserMessage(message: string)`** - Sets the default user-friendly message
2. **`addLocalizedMessage(lang: string, message: string)`** - Adds a localized message for a specific language (prevents duplicates)
3. **`updateLocalizedMessage(lang: string, message: string)`** - Updates or sets a localized message (allows overwriting)
4. **`getUserMessage(options?)`** - Retrieves the appropriate message based on language preferences
```typescript
import { BaseError } from "@shirudo/base-error";
class ValidationError extends BaseError<"ValidationError"> {
constructor(field: string, technicalReason: string) {
super(`Validation failed for field '${field}': ${technicalReason}`);
// Chain method calls for fluent API
this.withUserMessage("Please check your input and try again.")
.addLocalizedMessage("en", "Please check your input and try again.")
.addLocalizedMessage("es", "Por favor, revise su entrada e inténtelo de nuevo.")
.addLocalizedMessage("fr", "Veuillez vérifier votre saisie et réessayer.")
.addLocalizedMessage("de", "Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es erneut.");
}
}
const error = new ValidationError("email", "invalid format");
// Get message with different language preferences
error.getUserMessage(); // Default message
error.getUserMessage({ preferredLang: "es" }); // Spanish message
error.getUserMessage({ preferredLang: "it", fallbackLang: "en" }); // English (fallback)
error.getUserMessage({ preferredLang: "pt", fallbackLang: "it" }); // Default message (no match)
// Duplicate prevention
try {
error.addLocalizedMessage("en", "Another English message"); // Throws error
} catch (e) {
console.log(e.message); // "Localized message for language 'en' already exists..."
}
// Use updateLocalizedMessage to intentionally overwrite
error.updateLocalizedMessage("en", "Updated English message"); // Works fine
```
#### JSON Serialization with User Messages
User messages are automatically included in JSON serialization:
```typescript
const error = new ValidationError("email", "invalid format");
console.log(JSON.stringify(error, null, 2));
// Output:
// {
// "name": "ValidationError",
// "message": "Validation failed for field 'email': invalid format",
// "timestamp": 1704067200000,
// "timestampIso": "2025-01-01T00:00:00.000Z",
// "stack": "...",
// "userMessage": "Please check your input and try again.",
// "localizedMessages": {
// "en": "Please check your input and try again.",
// "es": "Por favor, revise su entrada e inténtelo de nuevo.",
// "fr": "Veuillez vérifier votre saisie et réessayer.",
// "de": "Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es erneut."
// }
// }
```
#### Language Fallback Strategy
The `getUserMessage()` method uses a three-tier fallback strategy:
1. **Preferred language** - If specified and available
2. **Fallback language** - If preferred is not available but fallback is
3. **Default message** - If neither preferred nor fallback languages are available
4. **`undefined`** - If no user messages have been set
```typescript
const error = new ValidationError("email", "invalid format");
// Only set some languages
error.withUserMessage("Default message")
.addLocalizedMessage("en", "English message")
.addLocalizedMessage("fr", "French message");
// Fallback examples
error.getUserMessage({ preferredLang: "fr" }); // → "French message"
error.getUserMessage({ preferredLang: "es", fallbackLang: "en" }); // → "English message"
error.getUserMessage({ preferredLang: "es", fallbackLang: "de" }); // → "Default message"
error.getUserMessage({ preferredLang: "es" }); // → "Default message"
```
### Error Codes with Union Types
For applications that need consistent error codes, you can use union types with BaseError:
```typescript
import { BaseError } from "@shirudo/base-error";
// Define your error codes as a union type
type ErrorCode =
| "USER_NOT_FOUND"
| "USER_NOT_AUTHORIZED"
| "USER_NOT_AUTHENTICATED"
| "USER_QUOTA_LIMIT_REACHED";
// Base class for all user-related errors
class UserError<T extends ErrorCode> extends BaseError<T> {
constructor(
public readonly code: T,
message: string,
public readonly userId?: string,
cause?: unknown,
) {
super(message, cause);
this.code = code;
}
// Override toJSON to include the error code
toJSON() {
return {
...super.toJSON(),
code: this.code,
userId: this.userId,
};
}
}
// Specific error classes
class UserNotFoundError extends UserError<"USER_NOT_FOUND"> {
constructor(userId: string) {
super("USER_NOT_FOUND", `User with ID ${userId} was not found`, userId);
}
}
class UserNotAuthorizedError extends UserError<"USER_NOT_AUTHORIZED"> {
constructor(userId: string, resource: string) {
super(
"USER_NOT_AUTHORIZED",
`User ${userId} is not authorized to access ${resource}`,
userId,
);
}
}
// Type-safe error handling
function handleUserError(error: unknown): void {
if (error instanceof UserError) {
// TypeScript knows the error code is from the ErrorCode union
switch (error.code) {
case "USER_NOT_FOUND":
console.log("→ Redirecting to user registration page");
break;
case "USER_NOT_AUTHORIZED":
console.log("→ Redirecting to access denied page");
break;
case "USER_NOT_AUTHENTICATED":
console.log("→ Redirecting to login page");
break;
case "USER_QUOTA_LIMIT_REACHED":
console.log("→ Showing upgrade options");
break;
}
}
}
```
### Type Narrowing with instanceof
```typescript
import { BaseError } from "@shirudo/base-error";
class NotFoundError extends BaseError<"NotFoundError"> {
constructor(resourceId: string) {
super(`Resource ${resourceId} not found`); // Using automatic name inference
}
}
class ValidationError extends BaseError<"ValidationError"> {
constructor(field: string, message: string) {
super(`${field}: ${message}`); // Using automatic name inference
this.field = field;
}
field: string;
}
function handleError(error: unknown) {
// Type narrowing with instanceof
if (error instanceof NotFoundError) {
// TypeScript knows this is a NotFoundError
// error.name has IntelliSense and is typed as "NotFoundError"
console.log(`Got a ${error.name} with message: ${error.message}`);
// Handle 404 case
} else if (error instanceof ValidationError) {
// TypeScript knows this is a ValidationError
// error.name is typed as "ValidationError" and field is available
console.log(`Validation failed for field: ${error.field}`);
console.log(`Error type: ${error.name}, message: ${error.message}`);
// Handle validation error
} else if (error instanceof BaseError) {
// TypeScript knows this is some kind of BaseError
// error.name is typed based on the generic parameter
console.log(`Unknown error type: ${error.name}`);
console.log(`Occurred at: ${error.timestampIso}`);
} else {
console.log("Unknown error:", error);
}
}
```
## Utilities
### `guard` function
The package includes a `guard` utility function for runtime assertions with TypeScript type narrowing:
```typescript
import { BaseError, guard } from "@shirudo/base-error";
class UserNotFoundError extends BaseError<"UserNotFoundError"> {
constructor(userId: string) {
super(`User ${userId} not found`);
}
}
class ValidationError extends BaseError<"ValidationError"> {
constructor(message: string) {
super(message);
}
}
// Basic usage
function processUser(user: User | null) {
// Assert that user exists, throw custom error if not
guard(user, new UserNotFoundError("current-user"));
// TypeScript now knows user is not null
console.log(user.name); // No TypeScript error
}
// Validation example
function validateEmail(email: string) {
const isValid = email.includes("@") && email.includes(".");
guard(isValid, new ValidationError("Invalid email format"));
// Continue with valid email
return email.toLowerCase();
}
// Works with any truthy/falsy values
function processArray(items: unknown[]) {
guard(items.length > 0, new ValidationError("Array cannot be empty"));
// Process non-empty array
return items.map((item) => String(item));
}
```
The `guard` function:
- Throws the provided `BaseError` instance when the condition is falsy
- Provides TypeScript type narrowing through assertion signatures
- Works with any truthy/falsy values, not just booleans
- Maintains the full error context and stack trace
## API
### `BaseError<T extends string>`
```typescript
class BaseError<T extends string> extends Error {
// Constructor with automatic name inference
constructor(message: string, cause?: unknown);
// Properties
readonly name: T; // Error type name (automatically inferred)
readonly timestamp: number; // Epoch-ms timestamp
readonly timestampIso: string; // ISO-8601 timestamp
readonly stack?: string; // Stack trace
readonly cause?: unknown; // Error cause (if provided)
// Methods
toJSON(): Record<string, unknown>; // Serialize to JSON
// User Message Methods (v2.1+)
withUserMessage(message: string): this; // Set default user-friendly message
addLocalizedMessage(lang: string, message: string): this; // Add localized message (prevents duplicates)
updateLocalizedMessage(lang: string, message: string): this; // Update/set localized message (allows overwriting)
getUserMessage(options?: {
preferredLang?: string;
fallbackLang?: string;
}): string | undefined; // Get appropriate user message
}
```
## TypeScript Support
This package is written in TypeScript and includes type definitions. The generic type parameter `T` allows you to specify the exact name of your error class for improved type safety.
## License
MIT