Exception
Python-inspired exception handling with chaining and notes
The Exception type provides Python-inspired exception handling with advanced features like exception chaining, contextual notes, and exception groups.
Why Use Exception?
Traditional JavaScript error handling has limitations:
- No error chaining: Can't track the original cause of an error
- Lost context: Error messages lose contextual information as errors propagate
- No grouping: Can't represent multiple related errors together
Exception solves these problems with Python-inspired features:
- Exception chaining - Track the root cause with
.from() - Contextual notes - Add debugging context with
.addNote() - Exception groups - Combine multiple related errors with
.group() - Namespaced exceptions - Organize errors by domain
- Type-safe comparison - Check errors with
.is()
Basic Usage
Creating Exceptions
Use the Exception namespace:
import { Exception } from '@deessejs/functions';
// Simple exception
const error = Exception({
name: "ValidationError",
message: "Email is required",
});
// With additional data
const error2 = Exception({
name: "DatabaseError",
message: "Connection failed",
data: { host: "localhost", port: 5432 },
});
// With namespace and code
const error3 = Exception({
name: "ConnectionFailed",
namespace: "database",
code: "DB_001",
message: "Failed to connect to database",
});Built-in Exception Helpers
Common exception types are built-in:
import { Exception } from '@deessejs/functions';
// Internal server error
Exception.internal("Unexpected error occurred");
// Database error
Exception.database("Failed to connect to database");
// Network error
Exception.network("Connection refused");
// Timeout
Exception.timeout("fetchUsers", 5000);
// { name: "TimeoutError", message: "Operation 'fetchUsers' timed out after 5000ms" }
// Not implemented
Exception.notImplemented("deleteUser");
// { name: "NotImplementedError", message: "Feature 'deleteUser' is not implemented" }
// Validation
Exception.validation("Email is invalid", { field: "email" });
// Not found
Exception.notFound("123", "User");
// { name: "NotFound", message: "User with ID '123' was not found" }
// Unauthorized
Exception.unauthorized("Invalid credentials");
// Forbidden
Exception.forbidden("Insufficient permissions");
// Conflict
Exception.conflict("Email already exists", { email: "test@example.com" });Exception Chaining
Track the root cause of errors with .from():
import { Exception } from '@deessejs/functions';
async function fetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (error) {
// Chain the network error as the cause
throw Exception({
name: "UserFetchFailed",
message: `Failed to fetch user ${id}`,
}).from(error);
}
}
// The exception now has a `cause` property
// error.cause is the original network errorChecking Causes
Check if an exception was caused by another:
try {
await fetchUser("123");
} catch (error) {
if (error.cause) {
console.log("Caused by:", error.cause.message);
}
}Contextual Notes
Add debugging context with .addNote():
import { Exception } from '@deessejs/functions';
const error = Exception({
name: "PaymentFailed",
message: "Payment processing failed",
})
.addNote("User ID: 123")
.addNote("Amount: $99.99")
.addNote("Payment method: credit_card")
.addNote("Timestamp: 2025-01-15T10:30:00Z");
// error.notes is an array of all notes
console.log(error.notes);
// [
// "User ID: 123",
// "Amount: $99.99",
// "Payment method: credit_card",
// "Timestamp: 2025-01-15T10:30:00Z"
// ]Notes for Debugging
Use notes to trace execution flow:
async function processPayment(userId: string, amount: number) {
try {
const user = await validateUser(userId);
if (!user.hasPaymentMethod) {
throw Exception({
name: "PaymentFailed",
message: "No payment method on file",
})
.addNote(`User ID: ${userId}`)
.addNote(`Attempted amount: $${amount}`)
.addNote("User state: active");
}
// ... process payment
} catch (error) {
// Add note even if exception already exists
if (error instanceof Error) {
error.addNote(`Processing failed at step: payment`);
}
throw error;
}
}Exception Groups
Combine multiple related errors with Exception.group():
import { Exception } from '@deessejs/functions';
async function fetchMultipleUsers(ids: string[]) {
const errors: Exception[] = [];
for (const id of ids) {
try {
await fetchUser(id);
} catch (error) {
errors.push(
Exception({
name: "UserFetchFailed",
message: `Failed to fetch user ${id}`,
}).from(error)
);
}
}
if (errors.length > 0) {
throw Exception.group("MultipleUsersFetchFailed", errors);
}
}
// Usage
try {
await fetchMultipleUsers(["123", "456", "789"]);
} catch (error) {
// error is an ExceptionGroup
// error.name === "MultipleUsersFetchFailed"
// error.exceptions contains all individual errors
}Accessing Grouped Errors
try {
await fetchMultipleUsers(["123", "456", "789"]);
} catch (error) {
if (error.exceptions) {
for (const exc of error.exceptions) {
console.log(`- ${exc.name}: ${exc.message}`);
}
}
}Namespaced Exceptions
Organize exceptions by domain with exceptionSpace():
import { exceptionSpace } from '@deessejs/functions/errors';
// Create a namespace for database-related exceptions
const Database = exceptionSpace({
name: "database",
code: "DB",
severity: "error",
});
// Define exceptions in the namespace
const ConnectionError = Database.define({
name: "ConnectionFailed",
code: "001",
message: "Failed to connect to database",
});
const QueryError = Database.define({
name: "QueryFailed",
code: "002",
message: "Query execution failed",
});
// Usage
throw ConnectionError({
message: "Connection timeout",
data: { host: "localhost", port: 5432 },
});Namespace Benefits
Namespaces provide:
- Organization - Group related exceptions
- Default codes - Auto-generate error codes
- Default severity - Set severity for all exceptions
- Type safety - Distinct types for different domains
Exception Comparison
Check exceptions by name, namespace, or instance:
import { Exception } from '@deessejs/functions';
const error = Exception({
name: "DatabaseError",
namespace: "database",
message: "Connection failed",
});
// Check by name
error.is("DatabaseError"); // true
error.is("NetworkError"); // false
// Check by namespace
error.is("database"); // true
error.is("network"); // false
// Check by instance
const otherError = Exception({ name: "DatabaseError" });
error.is(otherError); // trueUsing is() for Error Handling
try {
await riskyOperation();
} catch (error) {
if (error.is("database")) {
// Handle all database errors
console.log("Database error:", error.message);
} else if (error.is("network")) {
// Handle all network errors
console.log("Network error:", error.message);
} else {
// Handle unknown errors
console.log("Unknown error:", error.message);
}
}Working with Result
Use exceptions with Result types:
import { Exception, Result, tryCatch } from '@deessejs/functions';
function parseUser(json: string): Result<User, Exception> {
return tryCatch(() => {
const data = JSON.parse(json);
return userSchema.parse(data);
});
}
// Usage
const result = parseUser(invalidJson);
if (result.isFailure()) {
const error = result.error;
console.log(`Error: ${error.name}`);
console.log(`Message: ${error.message}`);
console.log(`Cause:`, error.cause);
}Working with Outcome
Use exceptions with Outcome types:
import { Exception, Outcome } from '@deessejs/functions';
async function login(email: string, password: string): PromiseOutcome<User, Cause, Exception> {
try {
const user = await db.users.findByEmail(email);
if (!user) {
return Outcome.failure(Cause.notFound(email, "User"));
}
if (!await verifyPassword(password, user.passwordHash)) {
return Outcome.failure(Cause.unauthorized("Invalid password"));
}
return Outcome.success(user);
} catch (error) {
// System error - use Exception
return Outcome.exception(
Exception.database("Failed to authenticate user").from(error)
);
}
}Advanced Patterns
Retry with Exception Chaining
import { Exception, retry, Schedule } from '@deessejs/functions';
async function fetchWithRetry(url: string) {
const lastError = await retry(
async () => {
const response = await fetch(url);
if (!response.ok) {
throw Exception.network(`HTTP ${response.status}`);
}
return response.json();
},
Schedule.exponential(1000).attempts(3)
);
// If retry fails, chain the last error
throw Exception({
name: "FetchFailed",
message: `Failed to fetch ${url} after retries`,
}).from(lastError);
}Exception Enrichment
Add context as exceptions propagate:
import { Exception } from '@deessejs/functions';
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId);
const user = await fetchUser(order.userId);
const payment = await processPayment(order);
return { order, user, payment };
} catch (error) {
// Enrich exception with order context
throw Exception({
name: "OrderProcessingFailed",
message: `Failed to process order ${orderId}`,
})
.from(error)
.addNote(`Order ID: ${orderId}`)
.addNote(`User ID: ${error.userId || "unknown"}`)
.addNote(`Timestamp: ${new Date().toISOString()}`);
}
}Conditional Exception Creation
import { Exception } from '@deessejs/functions';
function validateEmail(email: string): void {
const errors = [
!email.includes('@') && "Missing @ symbol",
!email.includes('.') && "Missing domain",
email.length < 5 && "Email too short",
].filter(Boolean) as string[];
if (errors.length > 0) {
throw Exception({
name: "ValidationError",
message: "Email validation failed",
}).addNote(`Email: ${email}`).addNote(`Errors: ${errors.join(", ")}`);
}
}Best Practices
1. Use Descriptive Names
// ✅ Good - specific and clear
Exception({ name: "UserNotFound", message: "..." });
Exception({ name: "PaymentDeclined", message: "..." });
// ❌ Bad - vague
Exception({ name: "Error", message: "..." });
Exception({ name: "Failed", message: "..." });2. Add Helpful Data
// ✅ Good - includes context
Exception({
name: "ValidationError",
message: "Invalid email format",
data: { email: "invalid", field: "email" },
});
// ❌ Bad - no context
Exception({ name: "ValidationError", message: "Invalid data" });3. Use Chaining for Root Causes
// ✅ Good - preserves root cause
throw Exception({
name: "UserFetchFailed",
message: "Failed to fetch user",
}).from(originalNetworkError);
// ❌ Bad - loses root cause
throw Exception({
name: "UserFetchFailed",
message: originalNetworkError.message,
});4. Add Notes for Debugging
// ✅ Good - notes help debugging
Exception({
name: "PaymentFailed",
message: "Payment processing failed",
})
.addNote(`User ID: ${userId}`)
.addNote(`Amount: $${amount}`)
.addNote(`Retry count: ${retryCount}`);
// ❌ Bad - no debugging context
Exception({
name: "PaymentFailed",
message: "Failed",
});5. Use Namespaces for Organization
// ✅ Good - organized by domain
const Database = exceptionSpace({ name: "database", severity: "error" });
const API = exceptionSpace({ name: "api", severity: "error" });
Database.define({ name: "ConnectionFailed" });
API.define({ name: "RateLimitExceeded" });
// ❌ Bad - flat structure
Exception({ name: "DatabaseConnectionFailed", namespace: "database" });
Exception({ name: "APIRateLimitExceeded", namespace: "api" });Exception vs Error
When to use Exception vs native Error:
// ✅ Use Exception for business logic errors
throw Exception.validation("Email is required");
throw Exception.notFound("123", "User");
throw Exception.conflict("Email already exists");
// ✅ Use Error for unexpected system errors
throw new Error("Unexpected type");
throw new TypeError("Expected string");
throw new ReferenceError("Variable not defined");Type Signatures
interface Exception {
name: string;
message: string;
namespace?: string;
code?: string;
data?: Record<string, any>;
cause?: Error | Exception;
notes?: string[];
from(cause: Error | Exception): this;
addNote(note: string): this;
is(nameOrNamespace: string | Exception): boolean;
}
function Exception(config: {
name: string;
message: string;
namespace?: string;
code?: string;
data?: Record<string, any>;
}): Exception;See Also
- Result - For success/failure without exceptions
- Outcome - For distinguishing failures from exceptions
- Try - For wrapping code that might throw
- Async Result - Async version of Result