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:
- 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.
- 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
- 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.
Summary
When the CLI emits a terminal
resultevent withis_error: true, an emptyerrorsarray, andsubtype: "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: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 theresulttext 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:With
errors == [],"; ".join([])is""(falsy), so it falls back tosubtype— which is"success"in this scenario. The actual human-readable error (e.g.API Error: Stream idle timeout - no chunks received) lives in theresultfield of the same message and would be the correct fallback.Exception(~line 852): after the CLI exits non-zero, the stored text replaces the transport'sProcessErrorand is re-raised viaraise Exception(error_text)— notProcessErroror anyClaudeSDKErrorsubclass. Downstream code withexcept (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
resulttext beforesubtype:(and arguably never use a subtype that doesn't start with
erroras error text.)2. Raise a typed error (e.g.
ProcessErrorwith the original exit code, or a dedicatedResultError(ClaudeSDKError)) instead of bareException.Happy to open a PR if maintainers agree with the direction.