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
// 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
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.
// `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.
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}');
}
}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:
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.
Connecting
connect throws rather than returning a Result.
// `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.
// 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.