Handle errors
How SDK calls fail and what to do about it.
Results, not exceptions
Management calls — videos, collections, live streams, player settings — return a result you check
rather than an exception you catch. That result is BunnyResult<T>: either Ok with the value, or Err with
a typed error.
when (val result = repo.getLiveStream(libraryId, streamId)) {
is BunnyResult.Ok -> render(result.value)
is BunnyResult.Err -> if (result.isTerminal) giveUp(result.message) else retryLater()
}
fold is shorter when both sides are one-liners. Note that success comes first:
repo.getLiveStream(libraryId, streamId).fold(
onOk = { stream -> render(stream) },
onErr = { error -> log(error.message) },
)
Also available: getOrNull(), errorOrNull() and map { }.
Terminal vs transient
This split is the one decision that matters in error handling here, and every error answers it
directly through isTerminal:
- Terminal —
401,403,404,410, plus the device-side cases below. The request will keep failing until something changes on your side or in the dashboard: wrong or revoked API key, a deleted stream or video, an ended stream. Do not retry in a loop; surface the state to the user. - Transient —
5xxand transport errors (status0). Retry with a delay, or just keep polling if you were polling.
if (error.isTerminal) showPermanentFailure(error.message) else scheduleRetry()
The error taxonomy
BunnyError has seven cases. Branch on the type when you want to react specifically; branch on
isTerminal when you only need to know whether retrying is worth it.
| Case | When | httpStatus |
isTerminal |
|---|---|---|---|
Auth |
401/403 — key missing, expired, or not allowed for this library |
401/403 |
true |
NotFound |
404 — wrong video, stream or library id |
404 |
true |
Http |
any other non-success status: 5xx, rate limiting, validation |
as returned | true only for 410 |
Network |
no usable response: DNS, connect, socket timeout, TLS, dropped connection | 0 |
false |
Decode |
the response arrived but did not match the expected shape | 0 |
false |
LocalFile |
the device could not read the file picked for upload | 0 |
true |
InvalidState |
the resource forbids the operation: an ended live stream, a stream key not issued yet | 0 |
varies — it says so itself |
when (error) {
is BunnyError.Auth -> promptForNewAccessKey()
is BunnyError.NotFound -> removeFromList()
else -> if (error.isTerminal) giveUp(error.message) else scheduleRetry()
}
error.message is a human-readable description, safe to log.
Polling
pollLiveStream returns the same envelope as everything else, so a polling loop reads
isTerminal to decide whether to stop:
when (val result = repo.pollLiveStream(libraryId, streamId)) {
is BunnyResult.Ok -> render(result.value)
is BunnyResult.Err -> if (result.isTerminal) stopPolling() else Unit // transient: keep going
}
Both players implement these rules internally; you only need them for your own management calls.
Upload errors
Uploads report failures as an UploadEvent.Failed value on the event stream, not as a thrown
exception — so a collect without a catch cannot miss one. The event carries the same
BunnyError as everything else:
uploader.observeUpload(uploadId)?.collect { event ->
if (event is UploadEvent.Failed) {
val videoId = event.videoId
if (!event.error.isTerminal && videoId != null) {
// Resumable path: continue from the offset the server already has
observe(tusVideoUploader.continueUpload(libraryId, videoId, uri))
} else {
showError(event.error.message)
}
}
}
LocalFile means the picked file could not be read — a different file is the only fix.
InvalidState from continueUpload means that uploader cannot resume; only the TUS one records
an offset.
See Upload videos for the full flow.
Player errors
The players show their own error states and recover from transient stream problems on their own.
To also handle playback errors in your code, set onPlaybackError on the player view:
player.onPlaybackError = { message ->
// log it, report it, show your own UI
}
Do not assign your own PlayerStateListener to the playback engine. There is one listener slot and
the player view owns it, so taking it over stops the built-in UI updating - the play/pause button,
the error banner, chapters and the seek bar all go stale. Everything the listener reports is
available as a callback on the view instead: onPlaybackError, onPlayingChanged,
onMutedChanged, onLoadingChanged, onPlaybackSpeedChanged, onChaptersUpdated,
onMomentsUpdated, onRetentionGraphUpdated, onPlayerTypeChanged and onVideoSizeChanged.
One envelope, everywhere
There is no second error style left to learn. In 3.x the generated REST clients (videosApi and
collectionsApi) sat on the public API and threw — ClientException for 4xx,
ServerException for 5xx — so every call site around them needed its own try/catch.
Those clients are gone from the public surface. Videos and collections now go through
videoRepository and collectionRepository, which answer with BunnyResult like everything
else, so the code on this page covers the whole SDK.