0003 — Typed errors instead of exceptions¶
- Status: accepted
- Date: 2026-09-21
- Deciders: BlueLib maintainers
Context¶
Android's Bluetooth API fails in three different ways:
- A status code inside a callback —
onCharacteristicRead(…, status)withstatus = 133. Nothing throws, and the caller has to remember that non-zero means failure. - A
booleanreturn value —requestMtu()returnsfalseand the platform forgets the request. - An exception —
SecurityExceptionwhenBLUETOOTH_CONNECTis missing,IOExceptionfrom a socket,IllegalArgumentExceptionfor a malformed UUID.
Applications must make decisions about these failures: retry, ask for a permission, tell the user the
device is out of range, or give up. A raw int or a bare exception is not enough to decide.
Decision¶
Every fallible operation returns BlueLibResult<T> (Success / Failure), and every failure is a
BlueLibError with:
- a stable
code(BlueLibErrorCode) that is safe to log or forward to analytics, isRetryable, derived from the platform status when one exists,- structured
context(device, operation, status, attempt), docsAnchorpointing at the matching Troubleshooting section.
The hierarchy is sealed, so a when over it stays exhaustive as the library grows.
Exceptions remain for the two cases where Kotlin requires a throwable:
BlueLibException(error)forgetOrThrow()and for failing aFlowmid-stream,BlueLibValidationExceptionfor input that is wrong regardless of the device (a malformed address, an out-of-range MTU), which is a programming error and should fail loudly in development.
Consequences¶
Good
- A UI can be driven entirely by the error taxonomy:
UNEXPECTED→ report,PERMISSION_MISSING→ ask,SCAN_THROTTLED→ waitretryAfterMillis,CONNECTION_LOST→ offer reconnect. - Diagnostics and errors share one representation, so what a bug report contains is what the code saw.
- Retry logic (
RetryPolicy) keys offisRetryable, so a non-retryable failure is never retried silently.
Bad
- Kotlin has no language-level support for "result types", so call sites use
.getOrThrow(),.getOrNull()oronFailure { }instead of exceptions propagating naturally. - Interop with Java callers is less idiomatic; they see
BlueLibResultand have to checkisSuccess.
Alternatives considered¶
- Throw everywhere. Rejected: it hides the availability question ("is this retryable?") and forces callers to catch a dozen exception types that the platform never documented.
- Kotlin's
Result<T>. Rejected: its failure case is aThrowable, so the taxonomy would have to be carried inside a throwable anyway, losing exhaustiveness and structured context.