Skip to content

query: raise ClaudeSDKError instead of bare Exception#771

Open
eeeasley wants to merge 1 commit intoanthropics:mainfrom
eeeasley:eeasley/use-sdk-error-hierarchy
Open

query: raise ClaudeSDKError instead of bare Exception#771
eeeasley wants to merge 1 commit intoanthropics:mainfrom
eeeasley:eeasley/use-sdk-error-hierarchy

Conversation

@eeeasley
Copy link
Copy Markdown

@eeeasley eeeasley commented Mar 28, 2026

All raise Exception(...) in _internal/query.py now use ClaudeSDKError so callers can catch SDK errors with a single except clause.

The receive_messages() error path (line 699) is the critical one — when the CLI subprocess exits non-zero and emits an error message, callers expecting except ClaudeSDKError: missed it entirely. We hit this in production: our wrapper caught (TooSlowError, ClaudeSDKError) but the bare Exception escaped and crashed the calling nursery.

7 sites converted; all were already protocol/process errors that fit the ClaudeSDKError hierarchy.

All raise Exception(...) calls in _internal/query.py now use
ClaudeSDKError so callers can catch SDK errors with a single
except clause instead of except Exception.

The receive_messages() error path (line 699) is the critical one:
when the CLI subprocess exits non-zero, callers expecting to catch
ClaudeSDKError missed it entirely.
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 12.50000% with 7 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@278570d). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/claude_agent_sdk/_internal/query.py 12.50% 7 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #771   +/-   ##
=======================================
  Coverage        ?   83.41%           
=======================================
  Files           ?       14           
  Lines           ?     2369           
  Branches        ?        0           
=======================================
  Hits            ?     1976           
  Misses          ?      393           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@eeeasley eeeasley marked this pull request as ready for review March 28, 2026 18:42
@andrew-vant
Copy link
Copy Markdown

I've been bitten by this, too. This is an improvement, but there's more to the problem; Query._read_messages eats the underlying exception before it ever gets to receive_messages. I don't know why, but I'm pretty sure the right thing is to attach the exception object so that receive_messages can reraise or raise from.

@km-anthropic
Copy link
Copy Markdown
Collaborator

@claude review

Comment on lines 15 to 21
ListToolsRequest,
)

from .._errors import ClaudeSDKError
from ..types import (
PermissionMode,
PermissionResultAllow,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Line 195 stores a bare Exception(response.get("error", "Unknown error")) in self.pending_control_results[request_id] when a control_response has subtype "error". This exception is later re-raised at line 396 via raise result in _send_control_request(), so callers catching except ClaudeSDKError: will miss it — the exact bug class this PR aims to fix. Since the import is already present, the fix is just Exception(ClaudeSDKError( on line 195.

Extended reasoning...

The missed conversion

This PR converts 7 raise Exception(...) sites to raise ClaudeSDKError(...), but misses an 8th site at line 195 inside _read_messages(). When a control_response message arrives with subtype == "error", the code stores a bare Exception object:

self.pending_control_results[request_id] = Exception(
    response.get("error", "Unknown error")
)

How the stored exception is re-raised

In _send_control_request() (around line 396), after the response event fires, the code retrieves the stored result and checks if it is an exception:

result = self.pending_control_results.pop(request_id)
self.pending_control_responses.pop(request_id, None)

if isinstance(result, Exception):
    raise result

Since the stored object is a bare Exception (not a ClaudeSDKError), this raise result throws a plain Exception. Any caller catching except ClaudeSDKError: will not catch it — the exception escapes up the stack.

Step-by-step proof

  1. The SDK sends a control request (e.g., initialize, mcp_status, interrupt, set_permission_mode, etc.) via _send_control_request().
  2. The CLI responds with a control_response message where subtype == "error".
  3. _read_messages() routes this to line 195, storing Exception("some error message") in self.pending_control_results[request_id].
  4. The event is set, unblocking _send_control_request().
  5. _send_control_request() pops the result, sees isinstance(result, Exception) is True, and executes raise result.
  6. A bare Exception propagates up. Callers using except ClaudeSDKError: (as described in the PR description’s production incident) do not catch it.
  7. The exception crashes the calling code.

Impact

This is arguably the most impactful missed conversion because it is the error path for all control request failures. Every public method that calls _send_control_request() is affected: initialize(), get_mcp_status(), interrupt(), set_permission_mode(), set_model(), rewind_files(), reconnect_mcp_server(), toggle_mcp_server(), and stop_task().

Fix

The ClaudeSDKError import is already present (line 18, added by this PR). The fix is a one-word change on line 195:

self.pending_control_results[request_id] = ClaudeSDKError(
    response.get("error", "Unknown error")
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants