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.
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.
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.
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.
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}
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.
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';
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';
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 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