# Error handling

The backend SDKs group errors into a small typed hierarchy so you can write narrow `catch` blocks and access structured fields directly, without parsing response bodies yourself.

## Exception classes

| Class                      | Raised when                                                                                                                  |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `StreamException`          | Base. Every SDK-emitted exception inherits from it.                                                                          |
| `StreamApiException`       | The server returned a 4xx or 5xx with the canonical error envelope, or returned a non-success body that could not be parsed. |
| `StreamRateLimitException` | The server returned HTTP 429. Subclass of `StreamApiException`. Adds `retryAfter`.                                           |
| `StreamTransportException` | A network-layer failure (connection reset, timeout, DNS, TLS) prevented the request from reaching the server.                |
| `StreamTaskException`      | An async task observed via the task-waiting helper ended with `status: "failed"`.                                            |

`StreamRateLimitException` extends `StreamApiException`, so a `catch (StreamApiException ...)` block matches 429s too. Catch the rate-limit subclass first if you want different handling.

## Class names per SDK

The hierarchy is the same across SDKs. Class names follow each language's idiom.

| Concept           | Go (sentinel error) | Python / Java / PHP        | Ruby                            | .NET                          |
| ----------------- | ------------------- | -------------------------- | ------------------------------- | ----------------------------- |
| Base              | `StreamError`       | `StreamException`          | `GetStreamRuby::StreamError`    | `GetStreamException`          |
| HTTP API error    | `ErrApiResponse`    | `StreamApiException`       | `GetStreamRuby::ApiError`       | `GetStreamApiException`       |
| HTTP 429          | `ErrRateLimited`    | `StreamRateLimitException` | `GetStreamRuby::RateLimitError` | `GetStreamRateLimitException` |
| Transport failure | `ErrTransport`      | `StreamTransportException` | `GetStreamRuby::TransportError` | `GetStreamTransportException` |
| Task failure      | `ErrTaskFailed`     | `StreamTaskException`      | `GetStreamRuby::TaskError`      | `GetStreamTaskException`      |

In Go, branch on the sentinel with `errors.Is(err, getstream.ErrApiResponse)` and extract fields by unwrapping to `*StreamError` via `errors.As`.

## Fields on the API exception

When the server returns a 4xx or 5xx with the standard error envelope, the API exception carries every documented field. Use these directly instead of re-parsing the response body.

| Field             | Description                                                                                                          |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| `statusCode`      | HTTP status code (e.g. 400, 404, 500).                                                                               |
| `code`            | Stream's numeric error code from the envelope. See the [API error codes](https://getstream.io/docs/platform/api-error-codes/) reference. |
| `message`         | Human-readable error message.                                                                                        |
| `exceptionFields` | Map of field name to validation message. Populated for 4xx validation errors. Empty otherwise.                       |
| `unrecoverable`   | Whether the server marked this error as unrecoverable. Honor this when composing retry logic.                        |
| `rawResponseBody` | The exact bytes of the response body, as a string. Useful for logging and diagnostics.                               |
| `moreInfo`        | Optional URL pointing to more documentation about the error.                                                         |
| `details`         | Optional structured payload with error-specific context.                                                             |

## Catching API errors

<Tabs>

```python label="Python"
from getstream.exceptions import StreamApiException, StreamRateLimitException

try:
    client.send_message(...)
except StreamRateLimitException as e:
    # 429; e.retry_after is a timedelta or None
    if e.retry_after:
        time.sleep(e.retry_after.total_seconds())
except StreamApiException as e:
    if e.status_code == 422:
        for field, msg in e.exception_fields.items():
            print(f"{field}: {msg}")
    if e.unrecoverable:
        raise
```

```ruby label="Ruby"
begin
  client.send_message(...)
rescue GetStreamRuby::RateLimitError => e
  sleep(e.retry_after || 1)
rescue GetStreamRuby::ApiError => e
  if e.status_code == 422
    e.exception_fields.each { |field, msg| warn "#{field}: #{msg}" }
  end
  raise if e.unrecoverable
end
```

```php label="PHP"
use GetStream\Exceptions\StreamApiException;
use GetStream\Exceptions\StreamRateLimitException;

try {
    $client->sendMessage(...);
} catch (StreamRateLimitException $e) {
    // $e->getRetryAfter() is ?int (seconds)
    sleep($e->getRetryAfter() ?? 1);
} catch (StreamApiException $e) {
    if ($e->getStatusCode() === 422) {
        foreach ($e->getExceptionFields() as $field => $msg) {
            error_log("$field: $msg");
        }
    }
    if ($e->isUnrecoverable()) {
        throw $e;
    }
}
```

```go label="Go"
import (
    "errors"
    "github.com/GetStream/getstream-go/v4"
)

_, err := client.SendMessage(ctx, ...)
var streamErr *getstream.StreamError
if errors.As(err, &streamErr) {
    if errors.Is(err, getstream.ErrRateLimited) {
        time.Sleep(streamErr.RetryAfter)
    } else if errors.Is(err, getstream.ErrApiResponse) {
        if streamErr.StatusCode == 422 {
            for field, msg := range streamErr.ExceptionFields {
                fmt.Printf("%s: %s\n", field, msg)
            }
        }
        if streamErr.Unrecoverable {
            return err
        }
    }
}
```

```csharp label="C#"
using GetStream;

try
{
    await client.SendMessageAsync(...);
}
catch (GetStreamRateLimitException ex)
{
    // 429; ex.RetryAfter is a TimeSpan?
    await Task.Delay(ex.RetryAfter ?? TimeSpan.FromSeconds(1));
}
catch (GetStreamApiException ex) when (ex.StatusCode == 422)
{
    foreach (var (field, msg) in ex.ExceptionFields)
    {
        Console.WriteLine($"{field}: {msg}");
    }
    if (ex.Unrecoverable)
    {
        throw;
    }
}
```

```java label="Java"
import io.getstream.exceptions.StreamApiException;
import io.getstream.exceptions.StreamRateLimitException;

try {
    client.sendMessage(...);
} catch (StreamRateLimitException e) {
    if (e.getRetryAfter() != null) {
        Thread.sleep(e.getRetryAfter().toMillis());
    }
} catch (StreamApiException e) {
    if (e.getStatusCode() == 422) {
        e.getExceptionFields().forEach((field, msg) -> System.out.println(field + ": " + msg));
    }
    if (e.isUnrecoverable()) {
        throw e;
    }
}
```

</Tabs>

## Transport errors

Transport errors are failures that happen before the server can respond: connection refused or reset, request timeout, DNS lookup failure, TLS handshake failure. The SDK catches these at the HTTP-client boundary and re-emits them as a typed transport exception with a categorized `errorType`. The original error is preserved on the cause chain.

This means callers do not have to catch `httpx.RequestError`, `HttpRequestException`, `Faraday::Error`, `GuzzleException`, `IOException`, or `net.Error` separately. One `catch` block covers all transport-layer failures.

### The errorType enum

| Value                  | When the SDK uses it                                                      |
| ---------------------- | ------------------------------------------------------------------------- |
| `connection_reset`     | Connection refused, reset, or closed prematurely.                         |
| `timeout`              | Read, write, or wall-clock deadline exceeded.                             |
| `dns_failure`          | Could not resolve the host.                                               |
| `tls_handshake_failed` | TLS / SSL handshake failed (certificate verify, protocol mismatch, etc.). |
| `unknown`              | Could not be classified into one of the above.                            |

The transport exception always exposes the underlying error via the language-native cause-chain accessor.

### Catching transport errors

<Tabs>

```python label="Python"
from getstream.exceptions import StreamTransportException

try:
    client.do_something()
except StreamTransportException as e:
    # e.error_type is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
    # e.__cause__ is the underlying httpx exception
    if e.error_type == "timeout":
        # back off and try again later
        ...
```

```ruby label="Ruby"
begin
  client.do_something
rescue GetStreamRuby::TransportError => e
  # e.error_type is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
  # e.cause is the underlying Faraday error
  if e.error_type == 'timeout'
    # back off and try again later
  end
end
```

```php label="PHP"
use GetStream\Exceptions\StreamTransportException;

try {
    $client->doSomething();
} catch (StreamTransportException $e) {
    // $e->getErrorType() is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
    // $e->getPrevious() is the underlying Guzzle exception
    if ($e->getErrorType() === 'timeout') {
        // back off and try again later
    }
}
```

```go label="Go"
import (
    "errors"
    "github.com/GetStream/getstream-go/v4"
)

_, err := client.DoSomething(ctx, ...)
if errors.Is(err, getstream.ErrTransport) {
    var streamErr *getstream.StreamError
    errors.As(err, &streamErr)
    // streamErr.ErrorType is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
    // errors.Unwrap(err) returns the underlying transport error
    if streamErr.ErrorType == "timeout" {
        // back off and try again later
    }
}
```

```csharp label="C#"
using GetStream;

try
{
    await client.DoSomethingAsync();
}
catch (GetStreamTransportException ex)
{
    // ex.ErrorType is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
    // ex.InnerException is the underlying HttpRequestException / TaskCanceledException / etc.
    if (ex.ErrorType == "timeout")
    {
        // back off and try again later
    }
}
```

```java label="Java"
import io.getstream.exceptions.StreamTransportException;

try {
    client.doSomething();
} catch (StreamTransportException e) {
    // e.getErrorType() is one of connection_reset / timeout / dns_failure / tls_handshake_failed / unknown
    // e.getCause() is the underlying IOException
    if ("timeout".equals(e.getErrorType())) {
        // back off and try again later
    }
}
```

</Tabs>

### Distinguishing transport errors from API errors

Transport errors happen when no HTTP response was received. API errors happen when the server responded with a 4xx or 5xx. Catch them separately if you want to react differently:

<Tabs>

```python label="Python"
from getstream.exceptions import StreamApiException, StreamTransportException

try:
    client.do_something()
except StreamApiException as e:
    # server responded with a 4xx/5xx
    handle_api_error(e)
except StreamTransportException as e:
    # network or transport-layer failure
    handle_transport_error(e)
```

```go label="Go"
_, err := client.DoSomething(ctx, ...)
if errors.Is(err, getstream.ErrApiResponse) {
    // server responded with a 4xx/5xx
} else if errors.Is(err, getstream.ErrTransport) {
    // network or transport-layer failure
}
```

</Tabs>

Both transport and API exceptions inherit from the SDK's base exception. Catch that base type if you want to handle all SDK-emitted errors uniformly.

## Cause-chain preservation

Every wrapping point preserves the underlying cause via the language-native mechanism. You can always recover the original exception when you need it for diagnostics or instrumentation.

| Language | Accessor             |
| -------- | -------------------- |
| Python   | `exc.__cause__`      |
| Go       | `errors.Unwrap(err)` |
| Java     | `exc.getCause()`     |
| PHP      | `$e->getPrevious()`  |
| Ruby     | `exc.cause`          |
| .NET     | `ex.InnerException`  |

## Migration notes

### Python: deprecated `StreamAPIException` alias

`getstream.base.StreamAPIException` (capital `API`) is preserved as a deprecated alias for `StreamApiException`. Existing `except StreamAPIException` blocks and `isinstance(exc, StreamAPIException)` checks keep working, with a one-time `DeprecationWarning` on import. Switch to the new spelling at your convenience. The alias is slated for removal in the following minor release.

### Ruby: deprecated `Stream::APIError` alias

`GetStreamRuby::APIError` (capital `API`) is preserved as a deprecated alias for `GetStreamRuby::ApiError`. First access emits a one-time `Kernel.warn`. Slated for removal in `v9.0`.

### PHP: `getCode()` semantics

`StreamApiException::getCode()` continues to return the HTTP status code, the pre-existing behavior inherited from `\Exception::getCode()`. Stream's canonical numeric error code from the response envelope is now exposed via the new `getApiErrorCode(): int`. Existing callers branching on `$e->getCode() === 429` continue to work. Switch to `getApiErrorCode()` if you need the envelope code instead of the HTTP status.

### .NET: per-status subclasses removed

The previously-published `GetStreamAuthenticationException`, `GetStreamValidationException`, and `GetStreamFeedException` are removed. They were never thrown by the SDK, so no existing `catch` block was reached. If you have defensive `catch` blocks for those types, replace them with status-code filters on `GetStreamApiException`.

```csharp label="C#"
// before (never matched)
catch (GetStreamAuthenticationException ex) { ... }

// after
catch (GetStreamApiException ex) when (ex.StatusCode is 401 or 403) { ... }
```


---

This page was last updated at 2026-08-07T13:10:43.788Z.

For the most recent version of this documentation, visit [https://getstream.io/docs/platform/error-handling/](https://getstream.io/docs/platform/error-handling/).