tryError Documentation
Success vs Error Paths
Understanding how tryError handles success and error cases
Core Concept
tryError uses a union type approach where functions return either a successful value or a TryError. This eliminates the need for try/catch blocks and makes error handling explicit and type-safe.
1import { tryAsync, isTryError } from '@try-error/core';
2
3// Function returns either User or TryError
4const result = await tryAsync(() => fetchUser('123'));
5
6if (isTryError(result)) {
7 // Error path - TypeScript knows result is TryError
8 console.error('Failed to fetch user:', result.message);
9 console.error('Error type:', result.type);
10 console.error('Context:', result.context);
11} else {
12 // Success path - TypeScript knows result is User
13 console.log('User loaded:', result.name);
14 console.log('Email:', result.email);
15 console.log('ID:', result.id);
16}
Key Benefits
- • Type Safety: TypeScript knows exactly which type you're working with
- • Explicit Handling: You must handle both success and error cases
- • No Exceptions: Errors are values, not thrown exceptions
- • Composable: Easy to chain and transform operations
- • Performance: Success path has <3% overhead, error path overhead is configurable
Performance Characteristics
tryError is designed with performance in mind, optimizing for the common success path while providing rich debugging information for errors.
Success Path Performance
The success path is the common case and has minimal overhead:
- • <3% overhead vs native try/catch
- • Direct value return (no wrapper objects)
- • No stack trace capture
- • No context cloning
- • Suitable for hot paths and loops
// Native try/catch: 100ms for 1M operations
try {
const result = JSON.parse(validJson);
} catch (e) {}
// tryError: 103ms for 1M operations (<3% overhead)
const result = trySync(() => JSON.parse(validJson));
if (!isTryError(result)) {
// Use result
}
Error Path Performance
The error path has higher overhead due to debugging features:
- • 20% to 120% overhead (configurable)
- • Stack trace capture: ~60% of total overhead
- • Context deep cloning: ~25% of total overhead
- • Source location: ~10% of total overhead
- • Timestamp generation: ~5% of total overhead
// Default config: Rich debugging info (100-120% overhead)
const error = trySync(() => JSON.parse(invalidJson));
// Full stack trace, source location, context, timestamp
// Minimal config: Bare essentials (50% overhead)
configure(ConfigPresets.minimal());
const error = trySync(() => JSON.parse(invalidJson));
// Just type and message, no expensive operations
🤔 Why is Error Overhead Acceptable?
In well-designed systems, errors occur rarely. If you're parsing valid JSON 99.9% of the time, the error overhead only affects 0.1% of operations.
Stack traces, source locations, and context make debugging much easier. The time saved debugging often outweighs the runtime overhead.
For high-error-rate scenarios (like user input validation), use minimal configuration to reduce overhead to just 50%.
Exception throwing and catching is inherently expensive. The additional overhead for better debugging is proportionally small.
Optimizing for Your Use Case
1import { configure, ConfigPresets } from '@try-error/core';
2
3// High-performance parsing (expected errors)
4function parseUserInput(input: string) {
5 // Use minimal config for validation scenarios
6 configure(ConfigPresets.minimal());
7
8 const result = trySync(() => JSON.parse(input));
9 if (isTryError(result)) {
10 return { valid: false, error: 'Invalid JSON' };
11 }
12 return { valid: true, data: result };
13}
14
15// API calls (unexpected errors)
16async function fetchCriticalData(id: string) {
17 // Use default config for better debugging
18 configure(ConfigPresets.development());
19
20 const result = await tryAsync(() => fetch(`/api/data/${id}`));
21 if (isTryError(result)) {
22 // Rich error info helps debug API issues
23 console.error('API Error:', {
24 message: result.message,
25 stack: result.stack,
26 source: result.source,
27 context: result.context
28 });
29 throw result;
30 }
31 return result;
32}
33
34// Mixed scenarios
35function processDataPipeline(data: unknown[]) {
36 // Use scoped configuration for different stages
37 const parseResults = data.map(item => {
38 // Minimal config for parsing (high error rate expected)
39 configure(ConfigPresets.minimal());
40 return trySync(() => validateAndParse(item));
41 });
42
43 // Full config for processing (errors are bugs)
44 configure(ConfigPresets.development());
45 const processResults = parseResults
46 .filter(r => !isTryError(r))
47 .map(item => trySync(() => processItem(item)));
48
49 return {
50 parsed: parseResults.filter(r => !isTryError(r)),
51 errors: parseResults.filter(isTryError)
52 };
53}
Type Narrowing with Guards
Type guards are essential for narrowing union types and enabling TypeScript to understand which path you're on. tryError provides several type guards for different scenarios.
isTryError Guard
The primary type guard for distinguishing between success and error results.
1import { tryAsync, isTryError, TryResult } from '@try-error/core';
2
3async function handleUserFetch(userId: string) {
4 const result = await tryAsync(() => fetchUser(userId));
5
6 // Type guard narrows the union type
7 if (isTryError(result)) {
8 // result is TryError here
9 return {
10 success: false,
11 error: result.message,
12 errorType: result.type
13 };
14 }
15
16 // result is User here
17 return {
18 success: true,
19 user: {
20 id: result.id,
21 name: result.name,
22 email: result.email
23 }
24 };
25}
26
27// Generic handling function
28function processResult<T>(result: TryResult<T, TryError>) {
29 if (isTryError(result)) {
30 console.error(`Operation failed: ${result.message}`);
31 return null;
32 }
33
34 console.log('Operation succeeded');
35 return result;
36}
isTrySuccess Guard
Alternative guard that checks for successful results, useful for filtering operations.
1import { tryAsync, isTrySuccess, isTryError } from '@try-error/core';
2
3async function fetchMultipleUsers(userIds: string[]) {
4 const results = await Promise.all(
5 userIds.map(id => tryAsync(() => fetchUser(id)))
6 );
7
8 // Filter successful results
9 const successfulUsers = results.filter(isTrySuccess);
10 // TypeScript knows successfulUsers is User[]
11
12 // Filter failed results
13 const failedResults = results.filter(isTryError);
14 // TypeScript knows failedResults is TryError[]
15
16 return {
17 users: successfulUsers,
18 errors: failedResults,
19 successCount: successfulUsers.length,
20 errorCount: failedResults.length
21 };
22}
23
24// Processing with success guard
25function processValidResults<T>(results: TryResult<T, TryError>[]) {
26 return results
27 .filter(isTrySuccess)
28 .map(result => {
29 // TypeScript knows result is T, not TryResult<T, TryError>
30 return processSuccessfulResult(result);
31 });
32}
hasErrorType Guard
Narrow errors by their specific type for targeted error handling. The hasErrorType
guard works with custom error types that you define in your application.
📝 Custom Error Types
The error types shown below (ValidationError
, AuthenticationError
, NetworkError
) are examples of custom error types you would define in your application. tryError doesn't provide these types built-in - you create them based on your domain needs.
1import { createTryError } from '@try-error/core';
2
3// Define your custom error types
4export const createValidationError = (message: string, field: string) =>
5 createTryError('ValidationError', message, { field });
6
7export const createAuthenticationError = (message: string) =>
8 createTryError('AuthenticationError', message);
9
10export const createNetworkError = (message: string, status: number, url: string) =>
11 createTryError('NetworkError', message, { status, url });
12
13// Use them in your functions
14async function performUserOperation(userId: string) {
15 if (!userId) {
16 throw createValidationError('User ID is required', 'userId');
17 }
18
19 const authResult = await checkAuthentication();
20 if (!authResult.valid) {
21 throw createAuthenticationError('Invalid credentials');
22 }
23
24 // ... rest of operation
25}
1import { tryAsync, isTryError, hasErrorType } from '@try-error/core';
2
3async function handleUserOperation(userId: string) {
4 const result = await tryAsync(() => performUserOperation(userId));
5
6 if (isTryError(result)) {
7 // Handle different error types specifically
8 if (hasErrorType(result, 'ValidationError')) {
9 // TypeScript knows result.type is 'ValidationError'
10 return {
11 status: 'validation_failed',
12 field: result.context?.field,
13 message: result.message
14 };
15 }
16
17 if (hasErrorType(result, 'AuthenticationError')) {
18 return {
19 status: 'auth_required',
20 redirectTo: '/login'
21 };
22 }
23
24 if (hasErrorType(result, 'NetworkError')) {
25 return {
26 status: 'network_error',
27 retryable: true,
28 retryAfter: 5000
29 };
30 }
31
32 // Generic error handling for unknown types
33 return {
34 status: 'unknown_error',
35 message: result.message
36 };
37 }
38
39 return {
40 status: 'success',
41 data: result
42 };
43}
Pattern Matching Approaches
Different ways to handle success and error cases based on your coding style and requirements.
Early Return Pattern
Handle errors early and continue with the success case.
1async function processUser(userId: string) {
2 // Fetch user
3 const userResult = await tryAsync(() => fetchUser(userId));
4 if (isTryError(userResult)) {
5 console.error('Failed to fetch user:', userResult.message);
6 return null;
7 }
8
9 // Validate user
10 const validationResult = await tryAsync(() => validateUser(userResult));
11 if (isTryError(validationResult)) {
12 console.error('User validation failed:', validationResult.message);
13 return null;
14 }
15
16 // Process user
17 const processResult = await tryAsync(() => processUserData(validationResult));
18 if (isTryError(processResult)) {
19 console.error('Processing failed:', processResult.message);
20 return null;
21 }
22
23 // Success path
24 console.log('User processed successfully');
25 return processResult;
26}
27
28// Alternative with explicit error handling
29async function processUserWithErrorHandling(userId: string) {
30 const userResult = await tryAsync(() => fetchUser(userId));
31 if (isTryError(userResult)) {
32 await logError(userResult);
33 await notifyAdmins(userResult);
34 return { success: false, error: userResult };
35 }
36
37 const user = userResult; // TypeScript knows this is User
38
39 // Continue processing...
40 return { success: true, data: user };
41}
Match Expression Pattern
Create a match function for more functional-style error handling.
1// Match utility function
2function match<T, E extends TryError, R>(
3 result: TryResult<T, E>,
4 handlers: {
5 success: (value: T) => R;
6 error: (error: E) => R;
7 }
8): R {
9 if (isTryError(result)) {
10 return handlers.error(result);
11 }
12 return handlers.success(result);
13}
14
15// Usage
16async function handleUserFetch(userId: string) {
17 const result = await tryAsync(() => fetchUser(userId));
18
19 return match(result, {
20 success: (user) => ({
21 status: 'success',
22 data: {
23 id: user.id,
24 name: user.name,
25 email: user.email
26 }
27 }),
28 error: (error) => ({
29 status: 'error',
30 message: error.message,
31 type: error.type,
32 retryable: isRetryableError(error)
33 })
34 });
35}
36
37// Advanced match with error type handling
38// Note: Uses the same custom error types (ValidationError, NetworkError, AuthenticationError)
39// defined in the hasErrorType section above
40function matchWithErrorTypes<T, R>(
41 result: TryResult<T, TryError>,
42 handlers: {
43 success: (value: T) => R;
44 validationError?: (error: TryError) => R;
45 networkError?: (error: TryError) => R;
46 authError?: (error: TryError) => R;
47 defaultError: (error: TryError) => R;
48 }
49): R {
50 if (isTryError(result)) {
51 if (hasErrorType(result, 'ValidationError') && handlers.validationError) {
52 return handlers.validationError(result);
53 }
54 if (hasErrorType(result, 'NetworkError') && handlers.networkError) {
55 return handlers.networkError(result);
56 }
57 if (hasErrorType(result, 'AuthenticationError') && handlers.authError) {
58 return handlers.authError(result);
59 }
60 return handlers.defaultError(result);
61 }
62 return handlers.success(result);
63}
Chain Pattern
Chain operations while preserving error information.
1import { mapResult, flatMapResult } from '@try-error/core';
2
3// Chain transformations
4async function processUserChain(userId: string) {
5 const userResult = await tryAsync(() => fetchUser(userId));
6
7 // Transform user to profile data
8 const profileResult = mapResult(userResult, user => ({
9 id: user.id,
10 displayName: `${user.firstName} ${user.lastName}`,
11 avatar: user.avatarUrl,
12 isActive: user.status === 'active'
13 }));
14
15 // Chain dependent operation
16 const enrichedResult = await (async () => {
17 if (isTryError(profileResult)) {
18 return profileResult;
19 }
20
21 return tryAsync(() => enrichProfile(profileResult));
22 })();
23
24 return enrichedResult;
25}
26
27// Using flatMapResult for cleaner chaining
28async function processUserFlatMap(userId: string) {
29 const userResult = await tryAsync(() => fetchUser(userId));
30
31 const profileResult = flatMapResult(userResult, user =>
32 tryAsync(() => createProfile(user))
33 );
34
35 const enrichedResult = flatMapResult(profileResult, profile =>
36 tryAsync(() => enrichProfile(profile))
37 );
38
39 return enrichedResult;
40}
41
42// Pipeline pattern
43async function processUserPipeline(userId: string) {
44 return tryAsync(() => fetchUser(userId))
45 .then(result => flatMapResult(result, user =>
46 tryAsync(() => validateUser(user))
47 ))
48 .then(result => flatMapResult(result, user =>
49 tryAsync(() => enrichUser(user))
50 ))
51 .then(result => flatMapResult(result, user =>
52 tryAsync(() => saveUser(user))
53 ));
54}
Error Recovery Strategies
Different approaches for recovering from errors and providing fallback behavior.
Fallback Values
1import { unwrapOr } from '@try-error/core';
2
3// Simple fallback
4async function getUserWithFallback(userId: string) {
5 const result = await tryAsync(() => fetchUser(userId));
6
7 // Provide default user if fetch fails
8 return unwrapOr(result, {
9 id: userId,
10 name: 'Unknown User',
11 email: 'unknown@example.com',
12 status: 'inactive'
13 });
14}
15
16// Conditional fallback
17function getUserOrGuest(userId: string) {
18 const result = trySync(() => getCachedUser(userId));
19
20 if (isTryError(result)) {
21 if (hasErrorType(result, 'NotFoundError')) {
22 return createGuestUser();
23 }
24 // For other errors, return null
25 return null;
26 }
27
28 return result;
29}
30
31// Computed fallback
32async function getUserWithComputedFallback(userId: string) {
33 const result = await tryAsync(() => fetchUser(userId));
34
35 if (isTryError(result)) {
36 console.warn(`Failed to fetch user ${userId}:`, result.message);
37
38 // Try to get from cache
39 const cacheResult = await tryAsync(() => getCachedUser(userId));
40 if (!isTryError(cacheResult)) {
41 return cacheResult;
42 }
43
44 // Create minimal user
45 return {
46 id: userId,
47 name: 'User',
48 email: '',
49 status: 'unknown'
50 };
51 }
52
53 return result;
54}
Retry Strategies
1// Simple retry
2async function fetchUserWithRetry(userId: string, maxAttempts = 3) {
3 let lastError: TryError;
4
5 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6 const result = await tryAsync(() => fetchUser(userId));
7
8 if (!isTryError(result)) {
9 return result;
10 }
11
12 lastError = result;
13
14 // Don't retry on certain error types
15 if (hasErrorType(result, 'ValidationError') ||
16 hasErrorType(result, 'AuthenticationError')) {
17 break;
18 }
19
20 // Wait before retry
21 if (attempt < maxAttempts) {
22 await sleep(1000 * attempt); // Exponential backoff
23 }
24 }
25
26 return lastError!;
27}
28
29// Conditional retry
30async function fetchWithConditionalRetry(userId: string) {
31 const result = await tryAsync(() => fetchUser(userId));
32
33 if (isTryError(result)) {
34 // Only retry network errors
35 if (hasErrorType(result, 'NetworkError')) {
36 console.log('Network error, retrying...');
37 return tryAsync(() => fetchUser(userId));
38 }
39
40 // Don't retry other errors
41 return result;
42 }
43
44 return result;
45}
46
47// Multiple fallback sources
48async function fetchUserMultiSource(userId: string) {
49 // Try primary source
50 const primaryResult = await tryAsync(() => fetchUserFromPrimary(userId));
51 if (!isTryError(primaryResult)) {
52 return primaryResult;
53 }
54
55 // Try secondary source
56 const secondaryResult = await tryAsync(() => fetchUserFromSecondary(userId));
57 if (!isTryError(secondaryResult)) {
58 return secondaryResult;
59 }
60
61 // Try cache
62 const cacheResult = await tryAsync(() => fetchUserFromCache(userId));
63 if (!isTryError(cacheResult)) {
64 return cacheResult;
65 }
66
67 // All sources failed
68 return primaryResult; // Return the first error
69}
Best Practices
✅ Do
- • Always use type guards to narrow union types
- • Handle errors explicitly rather than ignoring them
- • Use early returns for cleaner error handling
- • Provide meaningful fallback values when appropriate
- • Log errors with sufficient context for debugging
- • Use specific error type guards for targeted handling
- • Chain operations using flatMapResult for dependent calls
❌ Don't
- • Access properties without type guards
- • Ignore errors or fail silently
- • Use generic error handling for all error types
- • Retry operations that will never succeed
- • Create deeply nested if/else chains
- • Mix try/catch with tryError patterns
- • Return undefined or null instead of proper error handling
💡 Tips
- • Use match expressions for complex error handling logic
- • Consider creating domain-specific error handling utilities
- • Use mapResult for transforming successful values
- • Implement circuit breaker patterns for external services
- • Create error recovery strategies based on error types