Skip to content

Latest commit

 

History

History
225 lines (182 loc) · 11.3 KB

errors.md

File metadata and controls

225 lines (182 loc) · 11.3 KB

Error Handling

AssistantMessage Errors

AssistantMessage includes an error field for API-level errors:

ClaudeAgentSDK.query(prompt: "Hello") do |message|
  if message.is_a?(ClaudeAgentSDK::AssistantMessage) && message.error
    case message.error
    when 'rate_limit'            then puts "Rate limited - retry after delay"
    when 'authentication_failed' then puts "Check your API key"
    when 'billing_error'         then puts "Check your billing status"
    when 'invalid_request'       then puts "Invalid request format"
    when 'server_error'          then puts "Server error - retry later"
    end
  end
end

See examples/error_handling_example.rb.

Exception Handling

require 'claude_agent_sdk'

begin
  ClaudeAgentSDK.query(prompt: "Hello") { |message| puts message }
rescue ClaudeAgentSDK::ControlRequestTimeoutError
  puts "Control protocol timed out — consider increasing the timeout"
rescue ClaudeAgentSDK::CLINotFoundError
  puts "Please install Claude Code"
rescue ClaudeAgentSDK::ResultError => e
  # More specific than ProcessError — must be rescued first.
  case e.terminal_reason
  when 'api_error' then puts "API failed (HTTP #{e.api_error_status}): #{e.result}"
  else puts "Run failed (#{e.subtype}): #{e.errors.join('; ')}"
  end
rescue ClaudeAgentSDK::ProcessError => e
  puts "Process failed with exit code: #{e.exit_code}"
rescue ClaudeAgentSDK::CLIJSONDecodeError => e
  puts "Failed to parse response: #{e}"
end

An ordinary exception raised inside your own callbacks (hooks, can_use_tool, SDK MCP tool handlers, resource readers and prompt generators) is not raised to the code above. It is reported to the CLI as a failed callback, and the session keeps running. exit, Interrupt and other signal exceptions are the exception: the CLI gets the error response first, and then they propagate as Ruby normally would. See Hooks & Permission Callbacks and MCP Servers.

Terminal Error Results

When a run fails, the CLI emits a result message with is_error: true (which you still receive as a ResultMessage) and then exits non-zero on purpose, for shell-script consumers. That trailing process failure carries nothing beyond "exit code 1", so the SDK replaces it with a ResultError carrying the payload the CLI already reported — you can branch on why the run failed without matching on strings:

begin
  ClaudeAgentSDK.query(prompt: "...") { |m| handle(m) }
rescue ClaudeAgentSDK::ResultError => e
  retry_later    if e.terminal_reason == 'api_error'   # overloaded / timeout
  widen_budget   if e.subtype == 'error_max_turns'
  raise
end

ResultError subclasses ProcessError, so existing rescue ProcessError handlers keep working unchanged — but rescue ResultError first if you want the structured fields.

The exception message prefers, in order: the CLI's errors[], then result, then a non-success subtype, then the HTTP status. A run that ends on an API failure arrives as subtype: "success" with is_error: true and the prose in result, which is why subtype alone is not used as the message.

A refused resume (a nonexistent session, or a resume_drops_turn guard failure) reaches you the same way — including on a control request such as the initial handshake that was still in flight when the CLI exited. Match on Resume rejected by --resume-drops-turn: in the message and treat it as deterministic: clear the fork target and resume plainly rather than retrying.

Authentication Failures

The SDK holds no credentials; the CLI authenticates itself (see the README's Authentication). Neither of the two ways that can fail is reported when the session starts.

No credentials at all (a fresh container, a CI runner). The first prompt comes back as an AssistantMessage whose error is 'authentication_failed' (its text is "Not logged in · Please run /login", a command an SDK host cannot run), followed by a ResultMessage with is_error set. query() and ask then raise ResultError with terminal_reason == 'api_error'. A Client session stays open, so check the result's is_error there.

A key or token the API rejects. It ends the same way, with api_error_status 401, but only after the CLI has retried: ten APIRetryMessages over about three minutes when tested. Watch for them rather than waiting for the error:

ClaudeAgentSDK.query(prompt: "Hello") do |message|
  next unless message.is_a?(ClaudeAgentSDK::APIRetryMessage)

  warn "API retry #{message.attempt}/#{message.max_retries}: HTTP #{message.error_status} (#{message.error})"
end

Errors from Client Control Methods

Client#interrupt, #set_model, #set_permission_mode, #rewind_files, #reconnect_mcp_server, #toggle_mcp_server, #stop_task and the other control methods (the Ruby-style model= and permission_mode= included) send a request to the CLI and wait for its answer. They can fail in three ways:

What happened Raised
The client is not connected, or the connection ended while the request was waiting CLIConnectionError
The CLI did not answer in time ControlRequestTimeoutError
The CLI answered with an error: an MCP server name it does not know, a rewind without enable_file_checkpointing, a permission mode it does not accept a plain StandardError whose message is the CLI's text

The third is not a ClaudeSDKError, so rescue ClaudeAgentSDK::ClaudeSDKError does not catch it:

begin
  client.reconnect_mcp_server('my-server')
rescue ClaudeAgentSDK::ClaudeSDKError => e
  puts "Connection lost or timed out: #{e.message}"
rescue StandardError => e
  puts "The CLI refused: #{e.message}" # e.g. "Server not found: my-server"
end

Rescue StandardError for it, and do not test for the exact class.

Configuring Timeout

The control request timeout defaults to 1200 seconds (20 minutes) to accommodate long-running agent sessions. Override it via environment variable:

export CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS=300  # 5 minutes

Error Type Reference

# Base class of the SDK's error classes (all the ones below)
class ClaudeSDKError < StandardError; end

# Raised when connection to Claude Code fails
class CLIConnectionError < ClaudeSDKError; end

# Raised when the control protocol does not respond in time
class ControlRequestTimeoutError < CLIConnectionError; end

# Raised when Claude Code CLI is not found
class CLINotFoundError < CLIConnectionError
  # @param message [String] Error message (default: "Claude Code not found")
  # @param cli_path [String, nil] Optional path to the CLI that was not found
end

# Raised by CLIInstaller.install / .install_pinned (and the
# claude_agent_sdk:install_cli rake task) when the CLI cannot be installed.
# Never raised by query, ask or Client
class CLIInstallError < ClaudeSDKError; end

# Raised by the local-disk session APIs when CLAUDE_CONFIG_DIR is unset and
# no usable home directory exists for the default ~/.claude
class ConfigDirError < ClaudeSDKError; end

# Raised when resuming from a SessionStore fails: a store call raised or
# exceeded load_timeout_ms during resume materialization. #cause holds the
# adapter's exception (or the timeout)
class SessionStoreError < ClaudeSDKError; end

# Raised when the Claude Code process fails
class ProcessError < ClaudeSDKError
  attr_reader :exit_code,  # Integer | nil
              :stderr      # String | nil
end

# Raised when the CLI exits after reporting a terminal error result.
# Subclasses ProcessError, so existing `rescue ProcessError` keeps working.
class ResultError < ProcessError
  attr_reader :subtype,          # String | nil ('error_max_turns', 'error_during_execution', ...)
              :errors,           # Array<String> - error strings from the CLI (may be empty)
              :result,           # String | nil - result text; holds the "API Error: ..." prose
              :api_error_status, # Integer | nil - HTTP status of the failing API call
              :terminal_reason,  # String | nil - why the run ended ('api_error', 'max_turns', ...)
              :session_id,       # String | nil
              :data,             # Hash - raw `result` payload as emitted by the CLI
              :original_error    # ProcessError | nil - the bare exit error this replaced
end

# Raised when JSON parsing fails
class CLIJSONDecodeError < ClaudeSDKError
  attr_reader :line,           # String - The line that failed to parse
              :original_error  # Exception - The original JSON decode exception
end

# Raised when message parsing fails
class MessageParseError < ClaudeSDKError
  attr_reader :data  # Hash | nil
end
Error Description
ClaudeSDKError Base class of every error class in this table. One failure is not a ClaudeSDKError: a control request the CLI rejects, see Errors from Client Control Methods
CLIConnectionError Connection issues — including every write after a stdin write was cancelled mid-frame (the connection is unusable from then on — reconnect), and ClaudeAgentSDK.ask when the stream ends without a ResultMessage
ControlRequestTimeoutError Control protocol timeout (configurable via env var)
CLINotFoundError Claude Code not installed
CLIInstallError CLIInstaller.install / install_pinned (or the claude_agent_sdk:install_cli rake task) could not install the CLI: unsupported platform, invalid version, HTTP error, a response or download over its size limit, checksum mismatch, or a filesystem error. A wrapped failure keeps the original exception in #cause. This is the error a deploy or bin/setup script rescues; a failed install leaves a previously installed binary in place. See CLI installer
ConfigDirError A local-disk session API (list_sessions, get_session_*, rename_session, ...) could not locate the Claude config directory: CLAUDE_CONFIG_DIR is unset and there is no usable home directory (HOME unset with no passwd entry, as under docker --user in a minimal image, or an empty/relative HOME). Set CLAUDE_CONFIG_DIR
SessionStoreError Resuming from session_store: failed: a store call (#load, #list_sessions, #list_subkeys) raised or exceeded load_timeout_ms while the SDK materialized the transcript, before the CLI started. The message names the call; #cause is the adapter's exception. Before 1.0 this was a bare RuntimeError — see Sessions
ProcessError Process failed (includes exit_code and stderr) — also raised when the CLI is still running 5s after closing stdout and the SDK had to terminate it
ResultError Run ended on a terminal error result (subclasses ProcessError; adds subtype, errors, api_error_status, terminal_reason, ...) — rescue it first
CLIJSONDecodeError JSON parsing issues — including stdout ending mid-frame (a truncated final message; line holds the partial frame)
MessageParseError Message parsing issues

See lib/claude_agent_sdk/errors.rb for all error types.