claude-skills/

Anthropic公式スキル・プラグインの日本語ディレクトリ

last sync 22h ago
スキルOfficialdevelopment

🐛output-error-try-catch

プラグイン
outputai

説明

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 のリトライ機構が処理する前にエラーを横取りしてしまいます。 これにより組み込みのリトライロジックが無効化され、以下のような問題が発生します。

  1. リトライが発生しない: エラーが catch されるため、フレームワークがリトライすべきタイミングを認識できない
  2. エラー分類の誤り: FatalError として再スローすると、リトライが完全に抑制される
  3. エラーコンテキストの消失: 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 しない場合:

  1. ステップがエラーをスロー
  2. Output SDK がエラーを受け取る
  3. SDK がリトライポリシーを確認(ステップごとに設定)
  4. リトライ回数が残っていれば、ステップを再実行
  5. リトライ回数を使い切ると、完全なエラーコンテキストとともにワークフローが失敗

エラーを catch する場合:

  1. ステップがエラーをスロー
  2. catch ブロックがエラーを処理
  3. Output SDK は元のエラーを受け取れない
  4. リトライロジックがバイパスされる
  5. エラーの後続処理は実装者に委ねられる(多くの場合、誤った処理になる)

リトライ挙動の設定

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 を削除した後:

  1. 正常系のテスト: npx output workflow run <name> '<valid-input>'
  2. 異常系のテスト: ステップ失敗を引き起こす入力を使用する
  3. リトライ挙動の確認: 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:

  1. Retries not happening: The error is caught, so the framework doesn't know to retry
  2. Wrong error classification: Re-throwing as FatalError prevents retries entirely
  3. 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:

  1. Step throws an error
  2. Output SDK receives the error
  3. SDK checks retry policy (configured per step)
  4. If retries remain, step is re-executed
  5. If retries exhausted, workflow fails with full error context

When you DO catch errors:

  1. Step throws an error
  2. Your catch block handles it
  3. Output SDK never sees the original error
  4. Retry logic is bypassed
  5. 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:

  1. Test normal operation: npx output workflow run <name> '<valid-input>'
  2. Test failure scenarios: Use input that causes step failures
  3. 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 による自動翻訳です。