import { OpenloopError, ApduError, TransportError } from '@openloop/sdk-core'Thrown when the device returns an error status word — anything other
than 0x9000.
class ApduError extends OpenloopError {
readonly statusWord: number // e.g. 0x6985
}| Status word | Constant name | Description | Common cause |
|---|---|---|---|
0x9000 |
— | Success | — |
0x6985 |
Conditions not satisfied | Rejected, or the device is busy | The user pressed the reject button, or a request arrived while another confirmation was on screen (answered immediately on device firmware v1.1.5 and later) |
0x6a80 |
Invalid data | Invalid data | A malformed BIP44 path or transaction |
0x6a82 |
App not found | App not found | The matching app is not open on the device |
0x6d00 |
Instruction not supported | Instruction not supported | The device firmware version is too old |
0x6e00 |
CLA not supported | CLA not supported | Invalid class byte |
0x6f00 |
Internal error | Internal error | An unexpected error inside the device |
0x61XX |
More data | More data available | Intermediate response while fetching a PSBT (not an error) |
import { ApduError } from '@openloop/sdk-core'
try {
const sig = await eth.signPersonalMessage(DEFAULT_ETH_PATH, 'Hello')
} catch (err) {
if (err instanceof ApduError) {
if (err.statusWord === 0x6985) {
console.log('Rejected on the device, or the device is busy. Check the screen and try again')
} else {
console.log(`APDU error: 0x${err.statusWord.toString(16)}`)
}
}
}Thrown when a communication error occurs in the transport layer — USB, BLE, WebSocket, and so on.
class TransportError extends OpenloopError {
readonly code: TransportErrorCode // added in v0.6.0
// message carries English detail — for logs, not for display
}code
(added in v0.6.0)message is free-form English, so do not branch
on the string. Branch on code.
code |
Meaning | What to do |
|---|---|---|
not-supported |
This runtime cannot talk to the device at all (no WebHID / Web Bluetooth) | Point the user at a supported browser |
not-found |
No device matched, or the chooser was dismissed | Ask the user to pick again |
not-connected |
There is no open connection (never opened, or already closed) | Call open() before using it |
timeout |
The device did not answer in time | Ask the user to check the device — it may be waiting for approval |
disconnected |
The link dropped while in use | Try reconnecting |
io |
Anything else (framing, write failure, unexpected payload) | Log message |
codedefaults toio. Handle unknown values in adefaultbranch so new codes cannot break you.
In v0.6.0 the BLE, Safari and USB transports stopped
discarding the status word and now throw
ApduError. Previously these surfaced as
TransportError, so callers could not branch on
statusWord. Handle device-side failures (rejection,
conditions not satisfied, and so on) in the ApduError
branch.
import { OpenloopError, ApduError, TransportError } from '@openloop/sdk-core'
try {
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)
} catch (err) {
if (err instanceof ApduError) {
// Error response from the device
switch (err.statusWord) {
case 0x6985:
showMessage('Rejected on the device, or the device is busy. Check the screen and try again')
break
case 0x6a80:
showMessage('Invalid data')
break
default:
showMessage(`Device error: ${err.message}`)
}
} else if (err instanceof TransportError) {
// Communication error — branch on code, not on message
switch (err.code) {
case 'not-supported':
showMessage('This browser is not supported.')
break
case 'not-found':
showMessage('No device was found.')
break
case 'timeout':
showMessage('The device is not responding. Please check its screen.')
break
case 'disconnected':
case 'not-connected':
showMessage('Lost the connection to the device. Please reconnect.')
break
default:
showMessage(`Communication error: ${err.message}`)
}
} else if (err instanceof OpenloopError) {
// Any other SDK error
showMessage(`Error: ${err.message}`)
} else {
// Unexpected error
throw err
}
}async function withRetry<T>(
fn: () => Promise<T>,
reconnect: () => Promise<void>,
maxRetries: number = 1
): Promise<T> {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn()
} catch (err) {
if (err instanceof TransportError && i < maxRetries) {
await reconnect()
continue
}
throw err
}
}
throw new Error('Unreachable')
}
// Usage example
const { address } = await withRetry(
() => eth.getAddress(DEFAULT_ETH_PATH),
async () => {
transport = await WebHidTransport.reconnect()
eth = new EthereumApp(transport!)
}
)function isRejectedOrBusy(err: unknown): boolean {
return err instanceof ApduError && err.statusWord === 0x6985
}
try {
const sig = await eth.signTransaction(DEFAULT_ETH_PATH, rawTx)
} catch (err) {
if (isRejectedOrBusy(err)) {
// Rejected or busy — check the screen and try again
showMessage('Rejected on the device, or the device is busy. Check the screen and try again')
return
}
// Show every other error
showError(err)
}