The tryCatch
utility function provides several significant advantages over traditional try-catch blocks and Promise chains. Here's why you might want to consider this pattern:
Traditional error handling requires different approaches for synchronous and asynchronous code:
// Traditional approach - try catch
try {
const syncResult = JSON.parse(data);
} catch (error) {
handleError(error);
}
// Traditional Chain approach
asyncOperation()
.then(result => handleSuccess(result))
.catch(error => handleError(error));
With tryCatch
, you use the same pattern regardless of whether the operation is synchronous or asynchronous:
// tryCatch approach - consistent pattern
const [error, syncResult] = tryCatch(() => JSON.parse(data));
const [asyncError, asyncResult] = await tryCatch(async () => await asyncOperation());
Traditional try-catch can lead to forgotten error handling or unclear error states. The tuple return type forces explicit error checking and provides better type safety:
// Traditional approach - error handling can be forgotten
try {
const result = riskyOperation();
useResult(result); // Result might be undefined!
} catch {
// Empty catch block - bad practice but common
}
// tryCatch approach - explicit error handling
const [error, result] = await tryCatch(() => riskyOperation());
if (error) {
handleError(error);
return;
}
// TypeScript knows result is defined here
useResult(result);
Compare these approaches for handling errors in a function:
// Traditional approach - nested try-catch blocks
async function traditional() {
try {
const data = await fetchData();
try {
const processed = processData(data);
return processed;
} catch (processError) {
logger.error('Processing failed:', processError);
return null;
}
} catch (fetchError) {
logger.error('Fetch failed:', fetchError);
return null;
}
}
// tryCatch approach - flat and clear
async function withTryCatch() {
const [fetchError, data] = await tryCatch(() => fetchData());
if (fetchError) {
logger.error('Fetch failed:', fetchError);
return null;
}
const [processError, processed] = await tryCatch(() => processData(data));
if (processError) {
logger.error('Processing failed:', processError);
return null;
}
return processed;
}
The pattern excels when processing arrays of items where some operations might fail:
// Traditional approach - complex error handling
const results = await Promise.all(
items.map(async item => {
try {
const result = await processItem(item);
return { success: true, data: result };
} catch (error) {
return { success: false, error };
}
})
);
// tryCatch approach - cleaner and more explicit
const results = await Promise.all(
items.map(async item => await tryCatch(async () => await processItem(item)))
);
const { successes, failures } = results.reduce((acc, [error, result]) => {
if (error) {
acc.failures.push(error);
} else {
acc.successes.push(result);
}
return acc;
}, { successes: [], failures: [] });
The tuple return type makes testing error cases much more straightforward:
// Testing with tryCatch
test('handles parsing error', () => {
const [error, result] = tryCatch(() => JSON.parse('invalid'));
expect(error).toBeInstanceOf(SyntaxError);
expect(result).toBeUndefined();
});
test('handles successful parse', () => {
const [error, result] = tryCatch(() => JSON.parse('{"valid": true}'));
expect(error).toBeUndefined();
expect(result).toEqual({ valid: true });
});
This pattern provides a more flexible alternative to the proposed JavaScript feature "optional catch binding" (Error.try
). It gives you full control over error handling while maintaining code clarity and type safety.
This pattern particularly shines in scenarios where:
- You're building robust APIs that need to handle multiple potential failure points
- Working with external services or unreliable operations
- Processing large sets of data where partial failures should be handled gracefully
- Writing maintainable code that needs to be clear about error states
- Creating testable code with predictable error handling
The small overhead of writing [error, result]
instead of try-catch blocks pays off in terms of code maintainability, readability, and reliability.