Error Handling Guide

Error Class Hierarchy

Diagram 0

Import

import { OpenloopError, ApduError, TransportError } from '@openloop/sdk-core'

ApduError

Thrown when the device returns an error status word — anything other than 0x9000.

class ApduError extends OpenloopError {
  readonly statusWord: number  // e.g. 0x6985
}

APDU Status Word Table

Status word Constant name Description Common cause
0x9000 Success
0x6985 User rejected The user rejected on the device The user pressed the reject button
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)

Example

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('The user rejected the signing request on the device')
    } else {
      console.log(`APDU error: 0x${err.statusWord.toString(16)}`)
    }
  }
}

TransportError

Thrown when a communication error occurs in the transport layer — USB, BLE, WebSocket, and so on.

class TransportError extends OpenloopError {
  // message carries the error details
}

Common TransportErrors

Message Cause What to do
WalletConnect transport not connected WcTransport is not connected Call open() before using it
Transport type "..." not registered The transport is not registered with OpenloopSDK Register it with registerTransport()
No available transport found No transport is available Check the registered transports and the environment
Device disconnected The device was disconnected Try reconnecting
Exchange timeout The APDU response timed out Check the state of the device — it may be waiting for approval

Error Handling Patterns

The Basic Pattern

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')
        break
      case 0x6a80:
        showMessage('Invalid data')
        break
      default:
        showMessage(`Device error: ${err.message}`)
    }
  } else if (err instanceof TransportError) {
    // Communication error
    showMessage('Lost the connection to the device. Please reconnect.')
  } else if (err instanceof OpenloopError) {
    // Any other SDK error
    showMessage(`Error: ${err.message}`)
  } else {
    // Unexpected error
    throw err
  }
}

Retry Pattern With Reconnect

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!)
  }
)

Detecting a User Rejection

function isUserRejected(err: unknown): boolean {
  return err instanceof ApduError && err.statusWord === 0x6985
}

try {
  const sig = await eth.signTransaction(DEFAULT_ETH_PATH, rawTx)
} catch (err) {
  if (isUserRejected(err)) {
    // A deliberate user action — no error display needed
    return
  }
  // Show every other error
  showError(err)
}

Next Steps