Skip to content

is_error result with empty errors[] reports 'error result: success' and raises bare Exception, discarding the real error text #1146

Description

@bse-ai

Summary

When the CLI emits a terminal result event with is_error: true, an empty errors array, and subtype: "success" (which happens when the agent loop completes but the final message is an API error — e.g. a mid-stream gateway failure or stream idle timeout), the SDK produces the self-contradictory error message:

Claude Code returned an error result: success

and raises it as a bare Exception, so callers cannot catch it as a typed SDK error. The real error prose (which the CLI puts in the result text field) is discarded.

Version

claude-agent-sdk==0.2.123 (also present at current HEAD as far as I can tell).

Mechanism

Two compounding issues in claude_agent_sdk/_internal/query.py:

  1. Information-losing fallback (~lines 304-308):
if message.get("is_error"):
    errors = message.get("errors") or []
    self._last_error_result_text = "; ".join(errors) or str(message.get("subtype", "unknown error"))

With errors == [], "; ".join([]) is "" (falsy), so it falls back to subtype — which is "success" in this scenario. The actual human-readable error (e.g. API Error: Stream idle timeout - no chunks received) lives in the result field of the same message and would be the correct fallback.

  1. Bare Exception (~line 852): after the CLI exits non-zero, the stored text replaces the transport's ProcessError and is re-raised via raise Exception(error_text) — not ProcessError or any ClaudeSDKError subclass. Downstream code with except (ProcessError, CLIJSONDecodeError, CLINotFoundError) handlers never sees it and falls through to generic handlers, losing stderr/exit-code context.

Repro sketch

Any run whose final assistant turn is an API error while the loop itself completes: the CLI emits {"type":"result","subtype":"success","is_error":true,"errors":[],"result":"API Error: ..."}" and exits non-zero. Easiest to trigger behind a proxy/gateway that drops a stream mid-response.

Suggested fix

  1. Fall back to the result text before subtype:
self._last_error_result_text = (
    "; ".join(errors)
    or str(message.get("result") or "")
    or str(message.get("subtype", "unknown error"))
)

(and arguably never use a subtype that doesn't start with error as error text.)
2. Raise a typed error (e.g. ProcessError with the original exit code, or a dedicated ResultError(ClaudeSDKError)) instead of bare Exception.

Happy to open a PR if maintainers agree with the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions