# Exception Handling

The SDK groups 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](/chat/docs/java/api-errors-response/) 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
```

```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;
    }
}
```

```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;
    }
}
```

```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
```

</Tabs>

## 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-06-01T14:18:50.698Z.

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