From outputai
Fixes try-catch anti-patterns in Output SDK workflows that prevent retries, swallow errors, or cause unexpected FatalError wrapping. Use when step failures don't trigger retries.
npx claudepluginhub growthxai/output --plugin outputaiThis skill is limited to using the following tools:
This skill helps diagnose and fix a common anti-pattern where step calls are wrapped in try-catch blocks. This prevents Output SDK's retry mechanism from working properly and can lead to confusing error behavior.
Fixes direct I/O errors in Output SDK workflow functions causing hangs, undefined returns, determinism violations, or failed HTTP/DB ops. Moves operations to steps.
Provides error handling patterns like custom errors, structured logging, retries, circuit breakers for JavaScript/TypeScript apps including Express global handlers.
Guides error handling best practices to prevent silent failures, preserve context, and log effectively in try-catch blocks, propagation, and Result patterns.
Share bugs, ideas, or general feedback.
This skill helps diagnose and fix a common anti-pattern where step calls are wrapped in try-catch blocks. This prevents Output SDK's retry mechanism from working properly and can lead to confusing error behavior.
You're seeing:
When you wrap step calls in try-catch blocks, you intercept errors before the Output SDK retry mechanism can handle them. This defeats the built-in retry logic and can cause:
// WRONG: Error is silently ignored
try {
const result = await myStep(input);
} catch (error) {
console.log('Step failed'); // Swallowed!
return { success: false };
}
// WRONG: Turns retryable errors into fatal errors
try {
const result = await myStep(input);
} catch (error) {
throw new FatalError(error.message); // Prevents retries!
}
// WRONG: Loses error context and may affect retry behavior
try {
const result = await myStep(input);
} catch (error) {
throw new Error(`Step failed: ${error.message}`);
}
Let failures propagate naturally. Remove try-catch blocks around step calls and let the Output SDK handle errors:
export default workflow({
fn: async (input) => {
try {
const data = await fetchDataStep(input);
const result = await processDataStep(data);
return result;
} catch (error) {
throw new FatalError(error.message);
}
}
});
export default workflow({
fn: async (input) => {
const data = await fetchDataStep(input);
const result = await processDataStep(data);
return result;
}
});
There are limited cases where catching errors in workflows is valid:
When a step failure should trigger an alternative path:
export default workflow({
fn: async (input) => {
let data;
try {
data = await fetchFromPrimarySource(input);
} catch {
// Fallback to secondary source
data = await fetchFromSecondarySource(input);
}
return await processData(data);
}
});
When processing multiple items where some may fail:
export default workflow({
fn: async (input) => {
const results = [];
for (const item of input.items) {
try {
const result = await processItem(item);
results.push({ item, result, success: true });
} catch (error) {
results.push({ item, error: error.message, success: false });
}
}
return results; // Contains both successes and failures
}
});
Note: Even in these cases, be careful not to swallow errors that should cause the whole workflow to fail.
Search for the pattern:
# Find try blocks in workflow files
grep -rn "try {" src/workflows/
# Look for FatalError usage
grep -rn "FatalError" src/workflows/
Then review each match to see if it's wrapping step calls.
When you DON'T catch errors:
When you DO catch errors:
Instead of try-catch, configure retry policies on steps:
export const fetchData = step({
name: 'fetchData',
retry: {
maxAttempts: 3,
initialInterval: '1s',
maxInterval: '30s',
backoffCoefficient: 2
},
fn: async (input) => {
// If this fails, it will be retried according to policy
return await callApi(input);
}
});
FatalError is for errors that should NEVER be retried:
export const validateInput = step({
name: 'validateInput',
fn: async (input) => {
if (!input.userId) {
// This will never succeed on retry
throw new FatalError('userId is required');
}
return input;
}
});
Do NOT use FatalError to wrap other errors unless you're certain they shouldn't retry.
After removing try-catch:
npx output workflow run <name> '<valid-input>'npx output workflow debug <id>