try-error Documentation
Custom Error Types
Creating domain-specific error types and hierarchies for better error handling
Creating Custom Error Types
Custom error types provide better semantic meaning and enable more precise error handling in your application. They extend the basic TryError structure with domain-specific context.
Basic Custom Error
Simple Custom Error Typestypescript
1import { createTryError, TryError } from 'try-error';
2
3// Define custom error interfaces
4interface ValidationErrorContext {
5 field: string;
6 value: unknown;
7 rule: string;
8 expected?: string;
9}
10
11interface NetworkErrorContext {
12 url: string;
13 method: string;
14 status?: number;
15 statusText?: string;
16 timeout?: number;
17}
18
19interface BusinessErrorContext {
20 operation: string;
21 entityId: string;
22 entityType: string;
23 reason: string;
24}
25
26// Custom error creation functions
27export function createValidationError(
28 message: string,
29 context: ValidationErrorContext
30): TryError<ValidationErrorContext> {
31 return createTryError('ValidationError', message, context);
32}
33
34export function createNetworkError(
35 message: string,
36 context: NetworkErrorContext
37): TryError<NetworkErrorContext> {
38 return createTryError('NetworkError', message, context);
39}
40
41export function createBusinessError(
42 message: string,
43 context: BusinessErrorContext
44): TryError<BusinessErrorContext> {
45 return createTryError('BusinessError', message, context);
46}
47
48// Usage examples
49const validationError = createValidationError(
50 'Email format is invalid',
51 {
52 field: 'email',
53 value: 'invalid-email',
54 rule: 'email_format',
55 expected: 'valid email address'
56 }
57);
58
59const networkError = createNetworkError(
60 'Request timeout',
61 {
62 url: '/api/users/123',
63 method: 'GET',
64 timeout: 5000
65 }
66);
67
68const businessError = createBusinessError(
69 'Cannot delete user with active orders',
70 {
71 operation: 'delete_user',
72 entityId: '123',
73 entityType: 'User',
74 reason: 'has_active_orders'
75 }
76);
Type-Safe Error Handling
Type-Safe Custom Error Handlingtypescript
1import { isTryError, hasErrorType } from 'try-error';
2
3// Type guards for custom errors
4function isValidationError(error: TryError): error is TryError<ValidationErrorContext> {
5 return hasErrorType(error, 'ValidationError');
6}
7
8function isNetworkError(error: TryError): error is TryError<NetworkErrorContext> {
9 return hasErrorType(error, 'NetworkError');
10}
11
12function isBusinessError(error: TryError): error is TryError<BusinessErrorContext> {
13 return hasErrorType(error, 'BusinessError');
14}
15
16// Usage with type safety
17async function handleUserOperation(userId: string) {
18 const result = await tryAsync(() => performUserOperation(userId));
19
20 if (isTryError(result)) {
21 if (isValidationError(result)) {
22 // TypeScript knows result.context is ValidationErrorContext
23 return {
24 status: 'validation_failed',
25 field: result.context.field,
26 message: result.message,
27 expected: result.context.expected
28 };
29 }
30
31 if (isNetworkError(result)) {
32 // TypeScript knows result.context is NetworkErrorContext
33 return {
34 status: 'network_error',
35 url: result.context.url,
36 method: result.context.method,
37 retryable: result.context.status !== 404
38 };
39 }
40
41 if (isBusinessError(result)) {
42 // TypeScript knows result.context is BusinessErrorContext
43 return {
44 status: 'business_error',
45 operation: result.context.operation,
46 entityType: result.context.entityType,
47 reason: result.context.reason
48 };
49 }
50
51 // Generic error handling
52 return {
53 status: 'unknown_error',
54 message: result.message
55 };
56 }
57
58 return {
59 status: 'success',
60 data: result
61 };
62}
Error Hierarchies
Create hierarchical error structures that allow for both specific and general error handling.
Hierarchical Error Types
Error Type Hierarchytypescript
1// Base error categories
2type ErrorCategory = 'UserError' | 'SystemError' | 'ExternalError';
3
4// Specific error types within categories
5type UserErrorType =
6 | 'ValidationError'
7 | 'AuthenticationError'
8 | 'AuthorizationError'
9 | 'InputError';
10
11type SystemErrorType =
12 | 'DatabaseError'
13 | 'FileSystemError'
14 | 'ConfigurationError'
15 | 'InternalError';
16
17type ExternalErrorType =
18 | 'NetworkError'
19 | 'APIError'
20 | 'ServiceUnavailableError'
21 | 'ThirdPartyError';
22
23// Combined error type
24type AppErrorType = UserErrorType | SystemErrorType | ExternalErrorType;
25
26// Enhanced error context with hierarchy
27interface HierarchicalErrorContext {
28 category: ErrorCategory;
29 severity: 'low' | 'medium' | 'high' | 'critical';
30 retryable: boolean;
31 userFacing: boolean;
32 code?: string;
33 details?: Record<string, unknown>;
34}
35
36// Error creation with hierarchy
37function createHierarchicalError<T extends Record<string, unknown> = {}>(
38 type: AppErrorType,
39 message: string,
40 context: HierarchicalErrorContext & T
41): TryError<HierarchicalErrorContext & T> {
42 return createTryError(type, message, context);
43}
44
45// Category-specific error creators
46export function createUserError<T extends Record<string, unknown> = {}>(
47 type: UserErrorType,
48 message: string,
49 context: Omit<HierarchicalErrorContext, 'category'> & T
50): TryError<HierarchicalErrorContext & T> {
51 return createHierarchicalError(type, message, {
52 ...context,
53 category: 'UserError'
54 });
55}
56
57export function createSystemError<T extends Record<string, unknown> = {}>(
58 type: SystemErrorType,
59 message: string,
60 context: Omit<HierarchicalErrorContext, 'category'> & T
61): TryError<HierarchicalErrorContext & T> {
62 return createHierarchicalError(type, message, {
63 ...context,
64 category: 'SystemError'
65 });
66}
67
68export function createExternalError<T extends Record<string, unknown> = {}>(
69 type: ExternalErrorType,
70 message: string,
71 context: Omit<HierarchicalErrorContext, 'category'> & T
72): TryError<HierarchicalErrorContext & T> {
73 return createHierarchicalError(type, message, {
74 ...context,
75 category: 'ExternalError'
76 });
77}
78
79// Usage examples
80const validationError = createUserError('ValidationError', 'Invalid email format', {
81 severity: 'medium',
82 retryable: false,
83 userFacing: true,
84 code: 'INVALID_EMAIL',
85 field: 'email',
86 value: 'invalid@'
87});
88
89const databaseError = createSystemError('DatabaseError', 'Connection timeout', {
90 severity: 'high',
91 retryable: true,
92 userFacing: false,
93 code: 'DB_TIMEOUT',
94 connectionString: 'postgresql://...',
95 timeout: 5000
96});
97
98const apiError = createExternalError('APIError', 'Third-party service unavailable', {
99 severity: 'medium',
100 retryable: true,
101 userFacing: false,
102 code: 'EXTERNAL_API_DOWN',
103 service: 'payment-processor',
104 endpoint: '/api/payments'
105});
Hierarchical Error Handling
Category-Based Error Handlingtypescript
1// Type guards for error categories
2function isUserError(error: TryError): boolean {
3 return error.context?.category === 'UserError';
4}
5
6function isSystemError(error: TryError): boolean {
7 return error.context?.category === 'SystemError';
8}
9
10function isExternalError(error: TryError): boolean {
11 return error.context?.category === 'ExternalError';
12}
13
14// Severity-based handling
15function isCriticalError(error: TryError): boolean {
16 return error.context?.severity === 'critical';
17}
18
19function isRetryableError(error: TryError): boolean {
20 return error.context?.retryable === true;
21}
22
23function isUserFacingError(error: TryError): boolean {
24 return error.context?.userFacing === true;
25}
26
27// Comprehensive error handler
28class ErrorHandler {
29 async handleError(error: TryError): Promise<ErrorResponse> {
30 // Log error based on severity
31 this.logError(error);
32
33 // Alert on critical errors
34 if (isCriticalError(error)) {
35 await this.alertOncall(error);
36 }
37
38 // Handle by category
39 if (isUserError(error)) {
40 return this.handleUserError(error);
41 }
42
43 if (isSystemError(error)) {
44 return this.handleSystemError(error);
45 }
46
47 if (isExternalError(error)) {
48 return this.handleExternalError(error);
49 }
50
51 return this.handleUnknownError(error);
52 }
53
54 private handleUserError(error: TryError): ErrorResponse {
55 return {
56 status: 400,
57 code: error.context?.code || 'USER_ERROR',
58 message: isUserFacingError(error) ? error.message : 'Invalid request',
59 retryable: false,
60 details: isUserFacingError(error) ? error.context?.details : undefined
61 };
62 }
63
64 private handleSystemError(error: TryError): ErrorResponse {
65 return {
66 status: 500,
67 code: error.context?.code || 'SYSTEM_ERROR',
68 message: 'Internal server error',
69 retryable: isRetryableError(error),
70 details: undefined // Never expose system details
71 };
72 }
73
74 private handleExternalError(error: TryError): ErrorResponse {
75 return {
76 status: 502,
77 code: error.context?.code || 'EXTERNAL_ERROR',
78 message: 'Service temporarily unavailable',
79 retryable: isRetryableError(error),
80 details: undefined
81 };
82 }
83
84 private logError(error: TryError): void {
85 const logLevel = this.getLogLevel(error);
86 const logData = {
87 type: error.type,
88 message: error.message,
89 category: error.context?.category,
90 severity: error.context?.severity,
91 code: error.context?.code,
92 context: error.context,
93 timestamp: new Date().toISOString()
94 };
95
96 switch (logLevel) {
97 case 'error':
98 console.error('Error occurred:', logData);
99 break;
100 case 'warn':
101 console.warn('Warning:', logData);
102 break;
103 case 'info':
104 console.info('Info:', logData);
105 break;
106 }
107 }
108
109 private getLogLevel(error: TryError): 'error' | 'warn' | 'info' {
110 switch (error.context?.severity) {
111 case 'critical':
112 case 'high':
113 return 'error';
114 case 'medium':
115 return 'warn';
116 case 'low':
117 default:
118 return 'info';
119 }
120 }
121}
122
123interface ErrorResponse {
124 status: number;
125 code: string;
126 message: string;
127 retryable: boolean;
128 details?: Record<string, unknown>;
129}
Domain-Specific Error Systems
Create comprehensive error systems tailored to specific business domains.
E-commerce Error System
E-commerce Domain Errorstypescript
1// E-commerce specific error types
2type EcommerceErrorType =
3 | 'ProductNotFoundError'
4 | 'InsufficientInventoryError'
5 | 'InvalidPriceError'
6 | 'PaymentFailedError'
7 | 'ShippingUnavailableError'
8 | 'CouponInvalidError'
9 | 'OrderNotFoundError'
10 | 'CartEmptyError';
11
12// Domain-specific contexts
13interface ProductErrorContext {
14 productId: string;
15 sku?: string;
16 category?: string;
17 availability?: number;
18}
19
20interface PaymentErrorContext {
21 paymentMethod: string;
22 amount: number;
23 currency: string;
24 transactionId?: string;
25 gatewayResponse?: string;
26}
27
28interface OrderErrorContext {
29 orderId: string;
30 userId: string;
31 status: string;
32 items: Array<{ productId: string; quantity: number }>;
33}
34
35// E-commerce error factory
36class EcommerceErrorFactory {
37 static productNotFound(productId: string, sku?: string): TryError<ProductErrorContext> {
38 return createTryError('ProductNotFoundError', 'Product not found', {
39 productId,
40 sku,
41 category: 'product',
42 severity: 'medium',
43 userFacing: true,
44 retryable: false
45 });
46 }
47
48 static insufficientInventory(
49 productId: string,
50 requested: number,
51 available: number
52 ): TryError<ProductErrorContext> {
53 return createTryError('InsufficientInventoryError', 'Not enough items in stock', {
54 productId,
55 availability: available,
56 requested,
57 category: 'inventory',
58 severity: 'medium',
59 userFacing: true,
60 retryable: false
61 });
62 }
63
64 static paymentFailed(
65 paymentMethod: string,
66 amount: number,
67 currency: string,
68 gatewayResponse?: string
69 ): TryError<PaymentErrorContext> {
70 return createTryError('PaymentFailedError', 'Payment processing failed', {
71 paymentMethod,
72 amount,
73 currency,
74 gatewayResponse,
75 category: 'payment',
76 severity: 'high',
77 userFacing: true,
78 retryable: true
79 });
80 }
81
82 static orderNotFound(orderId: string, userId: string): TryError<OrderErrorContext> {
83 return createTryError('OrderNotFoundError', 'Order not found', {
84 orderId,
85 userId,
86 status: 'not_found',
87 items: [],
88 category: 'order',
89 severity: 'medium',
90 userFacing: true,
91 retryable: false
92 });
93 }
94
95 static invalidCoupon(
96 couponCode: string,
97 reason: 'expired' | 'invalid' | 'used' | 'minimum_not_met'
98 ): TryError {
99 const messages = {
100 expired: 'Coupon has expired',
101 invalid: 'Invalid coupon code',
102 used: 'Coupon has already been used',
103 minimum_not_met: 'Order does not meet minimum requirements for this coupon'
104 };
105
106 return createTryError('CouponInvalidError', messages[reason], {
107 couponCode,
108 reason,
109 category: 'promotion',
110 severity: 'low',
111 userFacing: true,
112 retryable: false
113 });
114 }
115}
116
117// Usage in e-commerce service
118class ProductService {
119 async getProduct(productId: string): Promise<TryResult<Product, TryError>> {
120 return tryAsync(async () => {
121 const product = await this.repository.findById(productId);
122
123 if (!product) {
124 throw EcommerceErrorFactory.productNotFound(productId, product?.sku);
125 }
126
127 return product;
128 });
129 }
130
131 async reserveInventory(
132 productId: string,
133 quantity: number
134 ): Promise<TryResult<void, TryError>> {
135 return tryAsync(async () => {
136 const product = await this.repository.findById(productId);
137
138 if (!product) {
139 throw EcommerceErrorFactory.productNotFound(productId);
140 }
141
142 if (product.inventory < quantity) {
143 throw EcommerceErrorFactory.insufficientInventory(
144 productId,
145 quantity,
146 product.inventory
147 );
148 }
149
150 await this.repository.updateInventory(productId, -quantity);
151 });
152 }
153}
154
155// Error handling in controllers
156class OrderController {
157 async createOrder(req: Request, res: Response) {
158 const result = await this.orderService.createOrder(req.body);
159
160 if (isTryError(result)) {
161 const response = this.mapErrorToResponse(result);
162 return res.status(response.status).json(response);
163 }
164
165 return res.status(201).json({ order: result });
166 }
167
168 private mapErrorToResponse(error: TryError): ErrorResponse {
169 switch (error.type) {
170 case 'ProductNotFoundError':
171 return {
172 status: 404,
173 code: 'PRODUCT_NOT_FOUND',
174 message: error.message,
175 details: { productId: error.context?.productId }
176 };
177
178 case 'InsufficientInventoryError':
179 return {
180 status: 409,
181 code: 'INSUFFICIENT_INVENTORY',
182 message: error.message,
183 details: {
184 productId: error.context?.productId,
185 available: error.context?.availability,
186 requested: error.context?.requested
187 }
188 };
189
190 case 'PaymentFailedError':
191 return {
192 status: 402,
193 code: 'PAYMENT_FAILED',
194 message: error.message,
195 retryable: true
196 };
197
198 default:
199 return {
200 status: 500,
201 code: 'INTERNAL_ERROR',
202 message: 'An unexpected error occurred'
203 };
204 }
205 }
206}
Error Composition and Chaining
Compose complex errors from simpler ones and maintain error chains for better debugging.
Error Composition
Composable Error Systemtypescript
1// Composable error builder
2class ErrorBuilder {
3 private errorData: {
4 type: string;
5 message: string;
6 context: Record<string, unknown>;
7 cause?: TryError;
8 chain: TryError[];
9 };
10
11 constructor(type: string, message: string) {
12 this.errorData = {
13 type,
14 message,
15 context: {},
16 chain: []
17 };
18 }
19
20 static create(type: string, message: string): ErrorBuilder {
21 return new ErrorBuilder(type, message);
22 }
23
24 withContext<T extends Record<string, unknown>>(context: T): ErrorBuilder {
25 this.errorData.context = { ...this.errorData.context, ...context };
26 return this;
27 }
28
29 withCause(cause: TryError): ErrorBuilder {
30 this.errorData.cause = cause;
31 return this;
32 }
33
34 withChain(errors: TryError[]): ErrorBuilder {
35 this.errorData.chain = [...this.errorData.chain, ...errors];
36 return this;
37 }
38
39 severity(level: 'low' | 'medium' | 'high' | 'critical'): ErrorBuilder {
40 return this.withContext({ severity: level });
41 }
42
43 retryable(canRetry: boolean = true): ErrorBuilder {
44 return this.withContext({ retryable: canRetry });
45 }
46
47 userFacing(isUserFacing: boolean = true): ErrorBuilder {
48 return this.withContext({ userFacing: isUserFacing });
49 }
50
51 code(errorCode: string): ErrorBuilder {
52 return this.withContext({ code: errorCode });
53 }
54
55 build(): TryError {
56 return createTryError(this.errorData.type, this.errorData.message, {
57 ...this.errorData.context,
58 ...(this.errorData.cause && { cause: this.errorData.cause }),
59 ...(this.errorData.chain.length > 0 && { chain: this.errorData.chain })
60 });
61 }
62}
63
64// Usage examples
65const composedError = ErrorBuilder
66 .create('OrderProcessingError', 'Failed to process order')
67 .withContext({
68 orderId: '12345',
69 userId: 'user123',
70 step: 'payment_processing'
71 })
72 .severity('high')
73 .retryable(true)
74 .code('ORDER_PROC_001')
75 .withCause(paymentError)
76 .build();
77
78// Error aggregation for batch operations
79class ErrorAggregator {
80 private errors: TryError[] = [];
81
82 add(error: TryError): void {
83 this.errors.push(error);
84 }
85
86 addAll(errors: TryError[]): void {
87 this.errors.push(...errors);
88 }
89
90 hasErrors(): boolean {
91 return this.errors.length > 0;
92 }
93
94 getErrors(): TryError[] {
95 return [...this.errors];
96 }
97
98 createAggregateError(type: string, message: string): TryError {
99 return ErrorBuilder
100 .create(type, message)
101 .withContext({
102 errorCount: this.errors.length,
103 errors: this.errors.map(e => ({
104 type: e.type,
105 message: e.message,
106 context: e.context
107 }))
108 })
109 .severity(this.getHighestSeverity())
110 .build();
111 }
112
113 private getHighestSeverity(): 'low' | 'medium' | 'high' | 'critical' {
114 const severities = this.errors
115 .map(e => e.context?.severity)
116 .filter(Boolean) as string[];
117
118 if (severities.includes('critical')) return 'critical';
119 if (severities.includes('high')) return 'high';
120 if (severities.includes('medium')) return 'medium';
121 return 'low';
122 }
123}
124
125// Batch processing with error aggregation
126async function processBatchWithAggregation<T>(
127 items: T[],
128 processor: (item: T) => Promise<TryResult<void, TryError>>
129): Promise<TryResult<void, TryError>> {
130 const aggregator = new ErrorAggregator();
131
132 for (const item of items) {
133 const result = await processor(item);
134 if (isTryError(result)) {
135 aggregator.add(result);
136 }
137 }
138
139 if (aggregator.hasErrors()) {
140 return aggregator.createAggregateError(
141 'BatchProcessingError',
142 `Failed to process ${aggregator.getErrors().length} out of ${items.length} items`
143 );
144 }
145
146 return undefined as any; // Success
147}
Best Practices
✅ Do
- • Create domain-specific error types for better semantics
- • Use hierarchical error structures for flexible handling
- • Include rich context information in custom errors
- • Implement type guards for type-safe error handling
- • Use error builders for complex error composition
- • Maintain error chains for debugging complex flows
- • Categorize errors by severity and user-facing nature
❌ Don't
- • Create too many granular error types without clear purpose
- • Include sensitive information in user-facing errors
- • Make error hierarchies too deep or complex
- • Ignore error context when handling errors
- • Use generic error types for domain-specific problems
- • Create circular references in error chains
- • Expose internal system details in error messages
💡 Tips
- • Start with basic custom errors and evolve to hierarchies
- • Use error codes for programmatic error handling
- • Document your error types and their contexts
- • Consider internationalization for user-facing messages
- • Use error aggregation for batch operations
- • Implement error recovery strategies based on error types