Skip to content

feat: Add Error Type Helper Functions (IsConnectionError, IsCLINotFoundError, etc.) #73

@severity1

Description

@severity1

Summary

Add convenience helper functions for checking error types, following Go SDK best practices for error handling ergonomics.

Python SDK Parity (100% Verified)

Python SDK Go SDK Fields Match
ClaudeSDKError SDKError + BaseError YES
CLIConnectionError ConnectionError YES
CLINotFoundError CLINotFoundError YES (Path)
ProcessError ProcessError YES (ExitCode, Stderr)
CLIJSONDecodeError JSONDecodeError YES (Line, Position, OriginalError)
MessageParseError MessageParseError YES (Data)

Note: Python SDK has NO Is*/As* helpers - these are Go-specific ergonomic additions following the `os.IsNotExist`, `os.IsPermission` pattern from the Go standard library.

Current State

The SDK defines 5 user-facing error types in `internal/shared/errors.go`:

Error Type Location Type Code Additional Fields
ConnectionError L34-49 "connection_error" -
CLINotFoundError L51-72 "cli_not_found_error" Path
ProcessError L74-104 "process_error" ExitCode, Stderr
JSONDecodeError L106-140 "json_decode_error" Line, Position, OriginalError
MessageParseError L142-159 "message_parse_error" Data

Current Usage Pattern (verbose):
```go
var connError *claudecode.ConnectionError
if errors.As(err, &connError) {
// Handle connection error
}

var cliError *claudecode.CLINotFoundError
if errors.As(err, &cliError) {
fmt.Printf("Path searched: %s\n", cliError.Path)
}
```

Proposed Addition

Is* Functions (Boolean Type Checks)

Add to `errors.go` after line 41:

```go
// IsConnectionError reports whether err is or wraps a ConnectionError.
func IsConnectionError(err error) bool {
var target *ConnectionError
return errors.As(err, &target)
}

// IsCLINotFoundError reports whether err is or wraps a CLINotFoundError.
func IsCLINotFoundError(err error) bool {
var target *CLINotFoundError
return errors.As(err, &target)
}

// IsProcessError reports whether err is or wraps a ProcessError.
func IsProcessError(err error) bool {
var target *ProcessError
return errors.As(err, &target)
}

// IsJSONDecodeError reports whether err is or wraps a JSONDecodeError.
func IsJSONDecodeError(err error) bool {
var target *JSONDecodeError
return errors.As(err, &target)
}

// IsMessageParseError reports whether err is or wraps a MessageParseError.
func IsMessageParseError(err error) bool {
var target *MessageParseError
return errors.As(err, &target)
}
```

As* Functions (Typed Extraction with Field Access)

```go
// AsConnectionError returns err as a *ConnectionError if it is one, nil otherwise.
func AsConnectionError(err error) *ConnectionError {
var target *ConnectionError
if errors.As(err, &target) {
return target
}
return nil
}

// AsCLINotFoundError returns err as a *CLINotFoundError if it is one, nil otherwise.
func AsCLINotFoundError(err error) *CLINotFoundError {
var target *CLINotFoundError
if errors.As(err, &target) {
return target
}
return nil
}

// AsProcessError returns err as a *ProcessError if it is one, nil otherwise.
func AsProcessError(err error) *ProcessError {
var target *ProcessError
if errors.As(err, &target) {
return target
}
return nil
}

// AsJSONDecodeError returns err as a *JSONDecodeError if it is one, nil otherwise.
func AsJSONDecodeError(err error) *JSONDecodeError {
var target *JSONDecodeError
if errors.As(err, &target) {
return target
}
return nil
}

// AsMessageParseError returns err as a *MessageParseError if it is one, nil otherwise.
func AsMessageParseError(err error) *MessageParseError {
var target *MessageParseError
if errors.As(err, &target) {
return target
}
return nil
}
```

New Usage Pattern (clean):
```go
// Simple boolean check
if claudecode.IsConnectionError(err) {
log.Println("Connection failed, will retry...")
return retryWithBackoff()
}

if claudecode.IsCLINotFoundError(err) {
fmt.Println("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code")
return
}

// Field access with As* helpers
if procErr := claudecode.AsProcessError(err); procErr != nil {
fmt.Printf("Process failed with exit code %d: %s\n", procErr.ExitCode, procErr.Stderr)
}

if cliErr := claudecode.AsCLINotFoundError(err); cliErr != nil {
fmt.Printf("CLI not found at path: %s\n", cliErr.Path)
}
```

Implementation Details

Files to Modify

File Action Details
`errors.go` ADD Add "errors" import, 5 Is* functions, 5 As* functions after L41
`errors_helpers_test.go` CREATE New test file (~150 lines)
`examples/08_client_advanced/main.go` UPDATE Use new helpers, remove "errors" import

Go Best Practices

Reference: Working with Errors in Go 1.13

  1. Use `errors.As()` internally - Correctly handles wrapped errors (unlike older `os.IsNotExist` pattern)
  2. Follow `os.IsNotExist` naming - Is* for boolean checks, consistent with stdlib
  3. Add As for field extraction* - Go-native pattern for typed error access
  4. Support wrapped errors - Helpers work with `fmt.Errorf("...: %w", err)`

Project Patterns (from internal/shared/errors.go)

  • All errors implement `SDKError` interface with `Type()` method
  • All errors embed `BaseError` for common functionality
  • `Unwrap()` implemented for error chaining
  • Comments follow Go doc conventions: `// FunctionName does X`

Benefits

  1. Reduced boilerplate - No need to declare typed variable for simple checks
  2. Discoverable API - IDE autocomplete shows available error checks
  3. Consistent with Go patterns - Similar to `os.IsNotExist`, `os.IsPermission`
  4. Preserves flexibility - Users can still use `errors.As` directly
  5. Field access - As* functions provide typed access to error fields

Acceptance Criteria

Implementation

  • Add `IsConnectionError(err error) bool`
  • Add `IsCLINotFoundError(err error) bool`
  • Add `IsProcessError(err error) bool`
  • Add `IsJSONDecodeError(err error) bool`
  • Add `IsMessageParseError(err error) bool`
  • Add `AsConnectionError(err error) *ConnectionError`
  • Add `AsCLINotFoundError(err error) *CLINotFoundError`
  • Add `AsProcessError(err error) *ProcessError`
  • Add `AsJSONDecodeError(err error) *JSONDecodeError`
  • Add `AsMessageParseError(err error) *MessageParseError`

Testing (errors_helpers_test.go)

  • Table-driven `TestIsErrorHelpers` covering all 5 types
  • Test wrapped errors (`fmt.Errorf` with `%w`)
  • Test nil error returns false/nil
  • Test generic error returns false/nil
  • `TestAsErrorHelpers` with field access validation
  • Verify Path, ExitCode, Stderr, Line, Position, Data field access

Documentation

  • Update `examples/08_client_advanced/main.go` to use new helpers
  • Add godoc comments following project conventions

Quality Gates

  • `go fmt ./...`
  • `go vet ./...`
  • `go build ./...`
  • `go test ./...` passes
  • `go test -cover ./...` shows coverage for new code

Priority

Low - Quality of life improvement, not blocking functionality.

Reference

Standard library patterns:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions