🐛output-error-try-catch
- プラグイン
- outputai
- ソース
- GitHub で見る ↗
説明
try-catchアンチパターンをOutput SDKワークフロー内で修正します。 次のような場合に使用: - リトライが正常に機能していない - エラーが握り潰されている - 予期しない`FatalError`ラッピングが発生している - ステップの失敗がリトライポリシーをトリガーしない
原文を表示
Fix try-catch anti-pattern in Output SDK workflows. Use when retries aren't working, errors are being swallowed, seeing unexpected FatalError wrapping, or when step failures don't trigger retry policies.
ユースケース
- ✓リトライが正常に機能していないとき
- ✓エラーが握り潰されているとき
- ✓予期しないFatalErrorラッピングが発生しているとき
- ✓ステップの失敗がリトライポリシーをトリガーしないとき
本文(日本語訳)
Try-Catch アンチパターンの診断と修正
概要
このスキルは、ステップ呼び出しを try-catch ブロックで囲むという一般的なアンチパターンの診断と修正を支援します。 この誤った実装は Output SDK のリトライ機構を正常に動作させなくなり、 わかりにくいエラー挙動を引き起こす可能性があります。
次のような場合に使用
- リトライが期待どおりに動作しない
- エラーが静かに握り潰されている
- 意図しない FatalError ラッピングが発生している
- ステップの失敗がリトライポリシーをトリガーしない
- エラーが catch されて誤った形で再スローされている
根本原因
ステップ呼び出しを try-catch ブロックで囲むと、Output SDK のリトライ機構が処理する前にエラーを横取りしてしまいます。 これにより組み込みのリトライロジックが無効化され、以下のような問題が発生します。
- リトライが発生しない: エラーが catch されるため、フレームワークがリトライすべきタイミングを認識できない
- エラー分類の誤り: FatalError として再スローすると、リトライが完全に抑制される
- エラーコンテキストの消失: catch ブロック内で元のエラー詳細が失われる可能性がある
症状
パターン 1: エラーの握り潰し
// 誤り: エラーが黙って無視される
try {
const result = await myStep( input );
} catch ( error ) {
console.log( 'Step failed' ); // 握り潰し!
return { success: false };
}
パターン 2: FatalError によるラッピング
// 誤り: リトライ可能なエラーを致命的エラーに変換してしまう
try {
const result = await myStep( input );
} catch ( error ) {
throw new FatalError( error.message ); // リトライが無効になる!
}
パターン 3: 汎用エラーとしての再スロー
// 誤り: エラーコンテキストが失われ、リトライ挙動に影響する可能性がある
try {
const result = await myStep( input );
} catch ( error ) {
throw new Error( `Step failed: ${error.message}` );
}
解決策
エラーを自然に伝播させてください。 ステップ呼び出し周辺の try-catch ブロックを削除し、Output SDK にエラー処理を委ねます。
修正前(誤り)
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;
}
} );
try-catch が適切なケース
ワークフロー内でエラーを catch することが有効な、限られたケースも存在します。
1. オプショナルステップ/フォールバックステップ
ステップの失敗が代替パスのトリガーとなるべき場合:
export default workflow( {
fn: async input => {
const data = await ( async () => {
try {
return await fetchFromPrimarySource( input );
} catch {
return await fetchFromSecondarySource( input );
}
} )();
return await processData( data );
}
} );
可読性を高めるために、IIFE を使用する代わりにフォールバックロジックを名前付きのヘルパー関数として切り出すことができます。
const fetchWithFallback = async input => {
try {
return await fetchFromPrimarySource( input );
} catch {
return await fetchFromSecondarySource( input );
}
};
export default workflow( {
fn: async input => {
const data = await fetchWithFallback( input );
return await processData( data );
}
} );
2. 部分的な失敗を含む集約結果
複数アイテムを処理する際に、一部が失敗しても処理を継続したい場合:
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; // 成功と失敗の両方を含む
}
} );
注意: これらのケースであっても、ワークフロー全体を失敗させるべきエラーを握り潰さないよう注意してください。
ステップを囲む try-catch の検索
以下のパターンで検索します。
# ワークフローファイル内の try ブロックを検索
grep -rn "try {" src/workflows/
# FatalError の使用箇所を検索
grep -rn "FatalError" src/workflows/
検索結果それぞれについて、ステップ呼び出しを囲んでいないか確認してください。
リトライの仕組み
エラーを catch しない場合:
- ステップがエラーをスロー
- Output SDK がエラーを受け取る
- SDK がリトライポリシーを確認(ステップごとに設定)
- リトライ回数が残っていれば、ステップを再実行
- リトライ回数を使い切ると、完全なエラーコンテキストとともにワークフローが失敗
エラーを catch する場合:
- ステップがエラーをスロー
- catch ブロックがエラーを処理
- Output SDK は元のエラーを受け取れない
- リトライロジックがバイパスされる
- エラーの後続処理は実装者に委ねられる(多くの場合、誤った処理になる)
リトライ挙動の設定
try-catch の代わりに、ステップに対してリトライポリシーを設定してください。
export const fetchData = step( {
name: 'fetchData',
retry: {
maxAttempts: 3,
initialInterval: '1s',
maxInterval: '30s',
backoffCoefficient: 2
},
fn: async input => {
// 失敗した場合、ポリシーに従ってリトライされる
return await callApi( input );
}
} );
FatalError の正しい使い方
FatalError は、絶対にリトライすべきでないエラーに使用します。
export const validateInput = step( {
name: 'validateInput',
fn: async input => {
if ( !input.userId ) {
// リトライしても成功しないエラー
throw new FatalError( 'userId is required' );
}
return input;
}
} );
リトライすべきでないことが確実な場合を除き、他のエラーを FatalError でラップして使用しないでください。
検証
try-catch を削除した後:
- 正常系のテスト:
npx output workflow run <name> '<valid-input>' - 異常系のテスト: ステップ失敗を引き起こす入力を使用する
- リトライ挙動の確認:
npx output workflow debug <id>でリトライの試行を確認する
関連情報
- リトライポリシーの設定については、ステップ定義のドキュメントを参照してください
- 想定内の失敗をより適切に処理するには、try-catch の代わりに条件分岐ロジックの使用を検討してください
原文(English)を表示
Fix Try-Catch Anti-Pattern
Overview
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.
When to Use This Skill
You're seeing:
- Retries not working as expected
- Errors being swallowed silently
- Unexpected FatalError wrapping
- Step failures not triggering retry policies
- Errors being caught and re-thrown incorrectly
Root Cause
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:
- Retries not happening: The error is caught, so the framework doesn't know to retry
- Wrong error classification: Re-throwing as FatalError prevents retries entirely
- Lost error context: Original error details may be lost in the catch block
Symptoms
Pattern 1: Errors Swallowed
// WRONG: Error is silently ignored
try {
const result = await myStep( input );
} catch ( error ) {
console.log( 'Step failed' ); // Swallowed!
return { success: false };
}
Pattern 2: FatalError Wrapping
// WRONG: Turns retryable errors into fatal errors
try {
const result = await myStep( input );
} catch ( error ) {
throw new FatalError( error.message ); // Prevents retries!
}
Pattern 3: Re-throwing Generic Errors
// WRONG: Loses error context and may affect retry behavior
try {
const result = await myStep( input );
} catch ( error ) {
throw new Error( `Step failed: ${error.message}` );
}
Solution
Let failures propagate naturally. Remove try-catch blocks around step calls and let the Output SDK handle errors:
Before (Wrong)
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 );
}
}
} );
After (Correct)
export default workflow( {
fn: async input => {
const data = await fetchDataStep( input );
const result = await processDataStep( data );
return result;
}
} );
When Try-Catch IS Appropriate
There are limited cases where catching errors in workflows is valid:
1. Optional/Fallback Steps
When a step failure should trigger an alternative path:
export default workflow( {
fn: async input => {
const data = await ( async () => {
try {
return await fetchFromPrimarySource( input );
} catch {
return await fetchFromSecondarySource( input );
}
} )();
return await processData( data );
}
} );
For readability, you can extract the fallback logic into a named helper function instead of using an IIFE:
const fetchWithFallback = async input => {
try {
return await fetchFromPrimarySource( input );
} catch {
return await fetchFromSecondarySource( input );
}
};
export default workflow( {
fn: async input => {
const data = await fetchWithFallback( input );
return await processData( data );
}
} );
2. Aggregate Results with Partial Failures
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.
Finding Try-Catch Around Steps
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.
How Retries Work
When you DON'T catch errors:
- Step throws an error
- Output SDK receives the error
- SDK checks retry policy (configured per step)
- If retries remain, step is re-executed
- If retries exhausted, workflow fails with full error context
When you DO catch errors:
- Step throws an error
- Your catch block handles it
- Output SDK never sees the original error
- Retry logic is bypassed
- You control what happens (often incorrectly)
Configuring Retry Behavior
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 );
}
} );
Using FatalError Correctly
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.
Verification
After removing try-catch:
- Test normal operation:
npx output workflow run <name> '<valid-input>' - Test failure scenarios: Use input that causes step failures
- Check retry behavior: Look for retry attempts in
npx output workflow debug <id>
Related Issues
- For configuring retry policies, see step definition documentation
- For handling expected failures gracefully, consider using conditional logic instead of try-catch
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。