HypenHypen
Device Capabilities

Errors

The ten device error codes, when each occurs, the platformDetail values you will see, and how to handle errors in each SDK

Errors

Device operations fail with values, not exceptions. A user dismissing a picker is an ordinary outcome, and your handler deals with it like any other result. Every failure carries one of ten codes. The set is closed: a new code would require a new protocol version.

CodeMeaningTypical causes
unsupportedThe capability, or the requested revision, isn't available on this connectionThe client doesn't offer it (no Web Bluetooth, no camera, a missing Info.plist key or manifest entry, the indicator not ready); lifetime: "background"; a host that can't represent a permission (contacts on the web)
unavailableThe capability exists, but the current host state or policy prevents itNo device plane (device-disabled); a call from onCreated before activation or from a deactivation or destroy handler (owner-inactive); a replayed dispatch (syncActions.replay); an undeclared native permission (not-declared:<name>); Bluetooth off (adapter-off); a host that can't show its UI right now
deniedThe user or the OS refusedCancel in the host consent dialog; a refused OS permission prompt
revokedA permission that was granted was withdrawn while the operation ranThe user turns off camera or microphone access mid-recording. Stop the affected work.
cancelledAn admitted operation was abandonedThe user dismissed the system picker, capture UI or chooser; you called cancel() or aborted the signal; the handler returned with the request still pending; the module deactivated; the user tapped Stop on a scan
timeoutThe overall deadline expiredThe user left the picker open past timeoutMs
throttledA prompt, rate or resource limit stopped the operationAnother prompt is already open (prompt-in-progress); a refusal cooldown is active (cooldown); the connection's upload budget is full; a live capture filled its bounded buffer while your consumer was slow
connectionLostThe transport, the server broker or the lease was lostThe socket closed; the lease wasn't renewed for 15 s; a Cloudflare Durable Object woke without its broker; the server process restarted
invalidParamsParameters, or data on the wire, violated the capability's schemaA bad argument (the detail names the field, e.g. params $.maxCount: …); initialCredit: 0; an uploaded item larger than 64 MiB or its declared size; a byte count or SHA-256 mismatch; a malformed message from the client
internalAn unexpected failure in the platform driver or the hostA bug or an unexpected platform error

platformDetail

error.platformDetail is short diagnostic text: either the server's reason for refusing a request locally, or the client's description of what happened. It helps with logging and debugging. Portable code must not branch on it. The values differ between platforms and aren't part of the protocol. Text that comes from the client is also untrusted input.

Values you'll commonly see:

DetailCodeMeaning
device-disabledunavailableNo device plane on this connection (the server called disableDevice() or uses allow-multiple sessions, the client has no device host, or the socket is compressed with shared history)
owner-inactiveunavailableThe module isn't in an active activation (see Lifetimes)
syncActions.replayunavailableThe call came from a replayed or broadcast dispatch
not-declared:<name>unavailableA native app didn't declare the permission
cooldownthrottledThe user recently refused this capability
prompt-in-progressthrottledAnother host prompt is open
picker-dismissedcancelledThe user closed the web file chooser

Handling errors

A missing capability is best handled before the request: check supports() and hide the button. For everything else, treat cancelled as quiet, show the rest, and log platformDetail:

.onAction("addPhoto", async ({ state, context }) => {
  const res = await context.device.request("gallery.pick", { mediaTypes: ["photo"], maxCount: 1 });
  if (res.ok) {
    state.photos.push(await storage.upload(res.value.items[0].bytes));
    return;
  }
  switch (res.error.code) {
    case "cancelled":
      return; // the user changed their mind
    case "denied":
      state.notice = "Photo access was declined.";
      break;
    case "throttled":
      state.notice = "Please try again in a moment.";
      break;
    case "unsupported":
    case "unavailable":
      state.notice = "This device can't pick photos right now.";
      break;
    default:
      state.notice = "Something went wrong.";
  }
  console.warn("gallery.pick failed", res.error.code, res.error.platformDetail);
})

Go

Errors are *device.Error values with a Code and a Detail. Match them with errors.Is and the sentinels (device.ErrDenied, device.ErrCancelled, …), or read the code with device.CodeOf(err). If your context.Context ends, the request is cancelled. The error is then cancelled, or timeout for a context deadline, and it unwraps to ctx.Err().

items, err := ctx.Device().Gallery().Pick(reqCtx, params)
switch {
case err == nil:
    // use items
case errors.Is(err, device.ErrCancelled):
    return
case errors.Is(err, device.ErrDenied):
    ctx.State.Notice = "Photo access was declined."
default:
    ctx.State.Notice = "Couldn't pick a photo (" + string(device.CodeOf(err)) + ")"
}

Kotlin

Calls return DeviceResult<T>, either Ok(value, simulated) or Err(DeviceFailure(code, platformDetail)). Helpers: getOrNull(), errorOrNull(), map, onOk, onErr, and getOrThrow(), which throws DeviceException. Only coroutine cancellation throws on its own. A failed events(...) or data(...) flow fails with DeviceException.

when (val r = ctx.device.gallery.pick(GalleryPickParams(listOf(MediaType.PHOTO), 1))) {
    is DeviceResult.Ok -> save(r.value.single().bytes)
    is DeviceResult.Err -> when (r.error.code) {
        DeviceErrorCode.CANCELLED -> Unit
        DeviceErrorCode.DENIED -> ctx.state.set("notice", "Photo access was declined.")
        else -> ctx.state.set("notice", "Couldn't pick a photo (${r.error.code.wireName})")
    }
}

Swift

Calls return Result<DeviceValue<T>, DeviceError>. DeviceError has code (.denied, .cancelled, …) and platformDetail.

switch await ctx.device.gallery.pick([.photo]) {
case .success(let picked):
    await save(picked.items[0].bytes)
case .failure(let error) where error.code == .cancelled:
    break
case .failure(let error):
    ctx.state.set("notice", "Couldn't pick a photo (\(error.code.rawValue))")
}

Rust

Local refusals come back at once as Err(DeviceError) from the call itself; everything else arrives as DeviceResult<Delivered<T>> when the call settles. DeviceError has code (a DeviceErrorCode), detail (the platformDetail or the broker's reason) and code_str() for the wire name. Nothing panics.

match ctx.expect("remote handler").device().gallery_pick(&[MediaType::Photo], 1) {
    Ok(call) => call.then(|s: &mut State, res| match res {
        Ok(items) => s.photo_len = items[0].bytes.len(),
        Err(e) if e.code == DeviceErrorCode::Cancelled => {}
        Err(e) if e.code == DeviceErrorCode::Denied => s.notice = "Photo access was declined.".into(),
        Err(e) => s.notice = format!("Couldn't pick a photo ({})", e.code_str()),
    }),
    Err(e) => state.notice = format!("Couldn't pick a photo ({})", e.code_str()),
}