# Error Handling

The SDK reports a failure in one of two ways. Almost every call returns a `Result<T>`, which is either a `Success` carrying the value or a `Failure` carrying the error, so failure is data and nothing throws past you unnoticed. A handful of calls throw instead, `connect` among them.

## Reading a Result

```dart label="Dart"
// Most SDK calls report failure as data rather than throwing, by returning a
// `Result<T>` that is either a `Success` or a `Failure`.
final result = await feed.getOrCreate();

switch (result) {
  case Success(data: final feedData):
    print('Loaded ${feedData.fid}');
  case Failure(error: final StreamFeedsException error):
    print('Could not load the feed: ${error.message}');
  case Failure(error: final error):
    print('Could not load the feed: $error');
}
```

`Failure.error` is typed `Object`, so narrow it before reading `message`.

### Unwrapping without matching

```dart label="Dart"
final result = await feed.getOrCreate();

// Throws the failure's exception
final feedData = result.getOrThrow();

// Or returns null instead
final maybeFeedData = result.getOrNull();

// And the booleans, when all you need is whether it worked
if (result.isFailure) return;
```

`getOrThrow` is how the code snippets throughout these docs keep to the point. In an app, prefer matching on the result so a failure has somewhere to go.

## The exception types

Every failure the SDK reports for work it attempted is a `StreamException`:

| Type                            | What it means                                                                                                                                                                 |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `StreamApiException`            | The server refused the request. Carries `statusCode`, `code`, `moreInfo`, `unrecoverable`, `retryAfter` and the raw `apiError`.                                               |
| `StreamNetworkException`        | The request did not get an answer. `isCancelled` marks a request the caller called off, `isTimeout` one that ran out of time.                                                 |
| `StreamAuthenticationException` | The token was rejected, or could not be produced.                                                                                                                             |
| `StreamClientException`         | A failure inside the SDK: wire data that would not decode, or an error thrown by app-supplied code the SDK ran. Worth reporting to a crash tracker rather than showing in UI. |

`StreamFeedsException` is an alias of the base type, so one `on` clause catches all four.

```dart label="Dart"
// `StreamFeedsException` aliases the base type, so one `on` clause catches
// all four subclasses.
try {
  final feedData = (await feed.getOrCreate()).getOrThrow();
} on StreamFeedsException catch (e) {
  print('${e.runtimeType}: ${e.message}');
}
```

`StreamApiError` is a different thing: it is the server's error payload, a wire model, not something the SDK throws. `StreamApiException.apiError` is where you find it.

### Telling failures apart

The hierarchy is `sealed`, so a `switch` over it is exhaustive.

```dart label="Dart"
final result = await feed.getOrCreate();
if (result case Failure(error: final error)) {
  switch (error) {
    case StreamApiException(:final statusCode, :final code):
      // The server refused the request. `statusCode` and `code` say why.
      print('API error $statusCode ($code)');
    case StreamNetworkException(isCancelled: true):
      // Usually the app navigating away, something to ignore rather than surface.
      break;
    case StreamNetworkException(isTimeout: final timedOut):
      print(timedOut ? 'Timed out' : 'The request did not reach the server');
    case StreamAuthenticationException():
      // The token was rejected. Sign the user out, or issue a fresh token.
      print('Authentication failed: ${error.message}');
    case StreamClientException():
      // A failure inside the SDK. Worth reporting to a crash tracker rather
      // than showing in UI.
      print('Client error: ${error.message}');
  }
}
```

>
> **Info:** A cancelled request is usually the app navigating away. It is something to ignore rather than surface.
>

## Deciding whether to retry

The SDK retries some of its own internal requests. This is the rule it applies, and a reasonable default for yours:

```dart label="Dart"
final result = await feed.getOrCreate();

// What the SDK itself treats as worth retrying: a network failure the caller
// did not cancel, and a server error. A rate limit is not retried here,
// because a fixed wait is not the wait a rate limit asks for.
return switch (result) {
  Success() => false,
  Failure(:final error) => switch (error) {
    StreamNetworkException(isCancelled: true) => false,
    StreamNetworkException() => true,
    StreamApiException(:final statusCode) => statusCode < 100 || statusCode >= 500,
    _ => false,
  },
};
```

A rate limit (`429`) is deliberately not in that set: it asks for a specific wait, which `retryAfter` on the exception carries. See [Rate Limits](https://getstream.io/activity-feeds/docs/flutter/rate-limits/).

## Connecting

`connect` throws rather than returning a `Result`.

```dart label="Dart"
// `connect` is one of the calls that throws rather than returning a Result.
// The connection attempt's own failure arrives as a StreamFeedsException,
// with the cause travelling on it.
try {
  await client.connect();
} on StreamFeedsException catch (e) {
  print('Connection failed: ${e.message} (${e.cause})');
}

// Calling connect again while a connection is established or in progress, or
// after `dispose`, is a programming error rather than a runtime failure: it
// throws a StateError. Guard on the connection's state instead of catching it.
```

Calling `connect` while a connection is established or in progress, or after `dispose`, throws a `StateError`. That is a programming error rather than a runtime failure, so guard on the connection's state rather than catching it.

An expired token needs no handling: the connection comes back with one the `TokenProvider` issued afterwards, without the app doing anything.

## Watching the connection

`client.connectionState` is what a "reconnecting" banner should follow. A disconnection carries the source that closed it, and whether it is worth reopening.

```dart label="Dart"
// The connection's state, for a banner while it is down. A disconnection
// carries its source, and a server-initiated one carries the failure behind
// it.
client.connectionState.listen((state) {
  switch (state) {
    case Connected():
      print('Connected');
    case Connecting() || Authenticating():
      print('Connecting...');
    case Disconnected(:final source):
      // `closeReason` reads the source in words, `cause` is the failure behind it
      // when there was one.
      print('Disconnected: ${source.closeReason}');
      print('Cause: ${source.cause}');
      print('Will reconnect: ${state.isAutomaticReconnectionEnabled}');
    case Initialized() || Disconnecting():
      break;
  }
});
```

## Seeing what went wrong

Turn on logging to see the requests and the WebSocket traffic behind a failure. See [Logging](https://getstream.io/activity-feeds/docs/flutter/logging/).

---

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