try-error Documentation

Success vs Error Paths

Understanding how try-error handles success and error cases

Core Concept

try-error 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';
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

Type Narrowing with Guards

Type guards are essential for narrowing union types and enabling TypeScript to understand which path you're on. try-error 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';
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';
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. try-error doesn't provide these types built-in - you create them based on your domain needs.

Defining Custom Error Typestypescript
1import { createTryError } from 'try-error';
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';
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';
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';
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 try-error 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 →