Skip to content

Write execution file in finally block so rate limit errors are parseable #1040

@jcf

Description

@jcf

I ran into this problem on a closed-source project that uses this action. Investigation with Claude Code turned up the following recommendation.

Problem

When Claude Code hits a rate limit, the SDK streams a rate_limit_event message followed by a result message with is_error: true, then throws. Because the execution file write in base-action/src/run-claude-sdk.ts is after the try/catch block (not in a finally), the file is never written and the execution_file output is empty.

This means callers cannot distinguish rate limits from other failures — both surface as exit code 1 with a minified SDK stack trace.

Where

base-action/src/run-claude-sdk.ts lines ~160–188:

try {
  for await (const message of query({ prompt, options: sdkOptions })) {
    messages.push(message);
    // ... messages are collected, including rate_limit_event and result
  }
} catch (error) {
  console.error("SDK execution error:", error);
  throw new Error(`SDK execution error: ${error}`);
  // ← execution file write below is unreachable
}

// This block never executes on rate limit errors
const result: ClaudeRunResult = { conclusion: "failure" };
try {
  await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
  result.executionFile = EXECUTION_FILE;
} catch (error) {
  core.warning(`Failed to write execution file: ${error}`);
}

Suggested fix

Move the execution file write into a finally block. The messages array is populated before the throw, so the data is available:

} catch (error) {
  console.error("SDK execution error:", error);
  throw new Error(`SDK execution error: ${error}`);
} finally {
  if (messages.length > 0) {
    try {
      await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
      core.setOutput("execution_file", EXECUTION_FILE);
    } catch (e) {
      core.warning(`Failed to write execution file: ${e}`);
    }
  }
}

Additionally, a dedicated error_type output (e.g. rate_limit) would let workflows handle rate limits without grepping JSON — but the execution file alone would unblock the use case.

Impact

Workflows using continue-on-error: true to post-process failures cannot detect rate limits vs real errors, making it impossible to:

  • Show a warning annotation instead of a hard failure
  • Surface the reset time to PR authors
  • Skip re-runs that will hit the same limit

Reproduction

Trigger a code review while rate-limited. The execution_file output is empty and the only signal is the generic SDK execution error: Error: Claude Code process exited with code 1 in the step log.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdev-experiencep2Non-showstopper bug or popular feature request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions