Exception: ClaudeAgentSDK::ResultError

Inherits:
ProcessError show all
Defined in:
lib/claude_agent_sdk/errors.rb

Overview

Raised when the CLI exits after reporting a terminal error result.

The CLI ends a failed run by emitting a result message with is_error: true (yielded to you as a ResultMessage) and then exiting non-zero, on purpose, for shell-script consumers. This exception replaces the bare "exit code 1" ProcessError for that case and carries the result's payload, so callers can branch on why the run failed without string matching:

begin
ClaudeAgentSDK.query(prompt: '...') { |message| ... }
rescue ClaudeAgentSDK::ResultError => e
if e.terminal_reason == 'api_error'   # e.g. overloaded / timeout
  retry_later
elsif e.subtype == 'error_max_turns'
  ...
end
end

It subclasses ProcessError, so existing rescue ProcessError handlers keep working.

Every structured field is type-narrowed: a payload whose subtype is not a String (or whose api_error_status is not an Integer, ...) reads back as nil rather than leaking the raw value, so callers can branch on these without re-validating. #data always holds the payload as the CLI sent it.

Instance Attribute Summary collapse

Attributes inherited from ProcessError

#exit_code, #stderr

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(message, data: nil, exit_code: nil, stderr: nil, original_error: nil) ⇒ ResultError

Returns a new instance of ResultError.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/claude_agent_sdk/errors.rb', line 167

def initialize(message, data: nil, exit_code: nil, stderr: nil, original_error: nil)
  data = {} unless data.is_a?(Hash)
  @data = data
  @original_error = original_error

  subtype = Payload.field(data, :subtype)
  @subtype = subtype.is_a?(String) ? subtype : nil
  @errors = Payload.normalize_errors(Payload.field(data, :errors))
  result = Payload.field(data, :result)
  @result = result.is_a?(String) ? result : nil
  status = Payload.field(data, :api_error_status)
  @api_error_status = status.is_a?(Integer) ? status : nil
  reason = Payload.field(data, :terminal_reason)
  @terminal_reason = reason.is_a?(String) ? reason : nil
  session_id = Payload.field(data, :session_id)
  @session_id = session_id.is_a?(String) ? session_id : nil

  super(message, exit_code: exit_code, stderr: stderr)
end

Instance Attribute Details

#api_error_statusObject (readonly)

HTTP status of the failing API call, if any.



82
83
84
# File 'lib/claude_agent_sdk/errors.rb', line 82

def api_error_status
  @api_error_status
end

#dataObject (readonly)

The raw result message payload as emitted by the CLI.



91
92
93
# File 'lib/claude_agent_sdk/errors.rb', line 91

def data
  @data
end

#errorsObject (readonly)

Error strings reported by the CLI (may be empty). Normalized the same way the exception text is built, so the two never disagree.



75
76
77
# File 'lib/claude_agent_sdk/errors.rb', line 75

def errors
  @errors
end

#original_errorObject (readonly)

The ProcessError this replaced (the bare "exit code 1" exit).

Ruby only populates #cause for an exception raised inside a rescue block; the read loop hands this one to the message queue instead of raising it there, so #cause is nil and the original exit error would be lost without an explicit accessor. Mirrors Python's cause chaining.



99
100
101
# File 'lib/claude_agent_sdk/errors.rb', line 99

def original_error
  @original_error
end

#resultObject (readonly)

The result text, if any. For API failures this holds the "API Error: ..." prose.



79
80
81
# File 'lib/claude_agent_sdk/errors.rb', line 79

def result
  @result
end

#session_idObject (readonly)

Session the result belongs to, if reported.



88
89
90
# File 'lib/claude_agent_sdk/errors.rb', line 88

def session_id
  @session_id
end

#subtypeObject (readonly)

The result subtype ("error_max_turns", "error_during_execution", ... — or "success" when the agent loop itself completed but the last turn was an API error).



71
72
73
# File 'lib/claude_agent_sdk/errors.rb', line 71

def subtype
  @subtype
end

#terminal_reasonObject (readonly)

Why the run ended (e.g. "api_error", "max_turns"), if reported.



85
86
87
# File 'lib/claude_agent_sdk/errors.rb', line 85

def terminal_reason
  @terminal_reason
end

Class Method Details

.error_text(data) ⇒ Object

Pick the most informative text from a result frame with is_error.

Terminal errors the CLI raises itself (error_max_turns, error_during_execution, ...) carry their prose in errors. A run that ends on an API failure instead arrives as subtype "success" with is_error true, an empty errors and the "API Error: ..." prose in result — falling back to the subtype there produced the self- contradictory "Claude Code returned an error result: success". Prefer errors, then result, then a non-success subtype, then the HTTP status, mirroring the TypeScript SDK's choice of result for the success subtype.

Public because the read loop builds the exception message from it, and because it is the documented way to get the same one-line summary out of a raw error result you already hold (an is_error ResultMessage the CLI emitted before exiting). Mirrors Python's _error_result_text.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/claude_agent_sdk/errors.rb', line 151

def self.error_text(data)
  errors = Payload.normalize_errors(Payload.field(data, :errors))
  return errors.join('; ') unless errors.empty?

  result = Payload.field(data, :result)
  return result.strip if result.is_a?(String) && !result.strip.empty?

  subtype = Payload.field(data, :subtype)
  return subtype if subtype.is_a?(String) && !subtype.empty? && subtype != 'success'

  status = Payload.field(data, :api_error_status)
  return "API error (HTTP #{status})" unless status.nil?

  'unknown error'
end