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.

Basic Success vs Error Patterntypescript
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
Success Path - Minimal Overheadtypescript
// 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
Error Path - Configurable Overheadtypescript
// 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?

1. Errors Should Be Exceptional

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.

2. Debugging Information is Valuable

Stack traces, source locations, and context make debugging much easier. The time saved debugging often outweighs the runtime overhead.

3. It's Configurable

For high-error-rate scenarios (like user input validation), use minimal configuration to reduce overhead to just 50%.

4. Errors Are Already Slow

Exception throwing and catching is inherently expensive. The additional overhead for better debugging is proportionally small.

Optimizing for Your Use Case

Performance Optimization Strategiestypescript
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.

isTryError Type Guardtypescript
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.

isTrySuccess Type Guardtypescript
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.

Defining Custom Error Typestypescript
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}
hasErrorType Type Guard Usagetypescript
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.

Early Return Patterntypescript
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.

Match Expression Patterntypescript
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.

Chain Patterntypescript
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

Fallback Value Strategiestypescript
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

Retry Strategiestypescript
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

Related Pages

Error Types

Learn about TryError structure and custom error types

View Error Types →

Custom Errors

Create domain-specific error types for better error handling

View Custom Errors →

Utilities API

Type guards, transformers, and utility functions

View Utils API →