> ## Documentation Index
> Fetch the complete documentation index at: https://docs.taprails.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle NFC and API errors gracefully in the TapRails SDK.

The SDK uses two typed error classes: `NFCPaymentError` for NFC-layer failures and `APIError` for backend communication failures. Both extend `Error` with a typed `code` for programmatic handling.

***

## NFCPaymentError

Thrown by `writeSignedInvoice`, `readSignedInvoice`, and the `useNFCMerchant` / `useNFCCustomer` hooks.

```ts theme={null}
class NFCPaymentError extends Error {
  code: NFCErrorCode;
}

enum NFCErrorCode {
  NOT_SUPPORTED   = 'NFC_NOT_SUPPORTED',   // Device has no NFC hardware
  NOT_ENABLED     = 'NFC_NOT_ENABLED',     // NFC is off in device settings
  READ_FAILED     = 'READ_FAILED',         // Tag read error (hardware or timing)
  WRITE_FAILED    = 'WRITE_FAILED',        // HCE emulation failed to start
  INVALID_DATA    = 'INVALID_DATA',        // Bad payload, expired, or invalid signature
  USER_CANCELLED  = 'USER_CANCELLED',      // User dismissed the NFC prompt
  API_ERROR       = 'API_ERROR',           // Backend error during NFC flow
  NETWORK_ERROR   = 'NETWORK_ERROR',       // No connection during NFC flow
  TIMEOUT         = 'TIMEOUT',            // 30s read timeout exceeded
  UNAUTHORIZED    = 'UNAUTHORIZED',        // Invalid API key
}
```

### Handling NFC errors

```tsx theme={null}
import { useNFCCustomer, NFCErrorCode } from '@taprails/tap-to-pay';

const { scanPaymentRequest, error } = useNFCCustomer();

const handleScan = async () => {
  try {
    await scanPaymentRequest();
  } catch (err) {
    // Error is also available via the `error` state
  }
};

useEffect(() => {
  if (!error) return;

  switch (error.code) {
    case NFCErrorCode.NOT_SUPPORTED:
      Alert.alert('NFC Not Supported', 'This device does not have NFC hardware.');
      break;
    case NFCErrorCode.NOT_ENABLED:
      Alert.alert(
        'NFC Disabled',
        'Please enable NFC in your device settings to use tap-to-pay.',
      );
      break;
    case NFCErrorCode.TIMEOUT:
      Alert.alert('Timed Out', 'Hold your phone closer to the payment terminal and try again.');
      break;
    case NFCErrorCode.INVALID_DATA:
      Alert.alert('Invalid Payment', 'This payment request is invalid or has expired.');
      break;
    case NFCErrorCode.USER_CANCELLED:
      // Silent — user chose to cancel, no alert needed
      break;
    case NFCErrorCode.UNAUTHORIZED:
      Alert.alert('Auth Error', 'Invalid API key. Please check your SDK configuration.');
      break;
    case NFCErrorCode.NETWORK_ERROR:
      Alert.alert('No Connection', 'Check your internet connection and try again.');
      break;
    default:
      Alert.alert('Error', error.message);
  }
}, [error]);
```

***

## APIError

Thrown by the API client layer (`createPayment`, `processPayment`, etc.) and surfaced via `usePayment`, `usePaymentStatus`, and related hooks.

```ts theme={null}
class APIError extends Error {
  code: APIErrorCode;
  statusCode?: number;   // HTTP status code
  details?: Record<string, any>;  // Additional error context from backend
}

enum APIErrorCode {
  NETWORK_ERROR        = 'NETWORK_ERROR',         // No connection / DNS failure
  TIMEOUT              = 'TIMEOUT',               // Request exceeded 30s
  UNAUTHORIZED         = 'UNAUTHORIZED',           // 401 — invalid API key
  BAD_REQUEST          = 'BAD_REQUEST',            // 400 — validation error
  NOT_FOUND            = 'NOT_FOUND',              // 404 — resource not found
  SERVER_ERROR         = 'SERVER_ERROR',           // 5xx — backend error
  UNKNOWN_ERROR        = 'UNKNOWN_ERROR',          // Unexpected error
  SDK_NOT_INITIALIZED  = 'SDK_NOT_INITIALIZED',   // initialize() not called yet
}
```

### Handling API errors

```tsx theme={null}
import { usePayment, APIErrorCode } from '@taprails/tap-to-pay';

const { createPaymentRequest, error } = usePayment();

useEffect(() => {
  if (!error) return;

  switch (error.code) {
    case APIErrorCode.UNAUTHORIZED:
      // Likely a bad API key or expired session
      redirectToLogin();
      break;
    case APIErrorCode.BAD_REQUEST:
      Alert.alert('Invalid Request', error.details?.message || error.message);
      break;
    case APIErrorCode.NOT_FOUND:
      Alert.alert('Not Found', 'Payment ID not found. It may have expired.');
      break;
    case APIErrorCode.SERVER_ERROR:
      Alert.alert('Server Error', 'Something went wrong on our end. Please try again.');
      break;
    case APIErrorCode.NETWORK_ERROR:
    case APIErrorCode.TIMEOUT:
      Alert.alert('Connection Error', 'Check your internet connection and retry.');
      break;
    case APIErrorCode.SDK_NOT_INITIALIZED:
      // Should not happen in production — initialize() called before rendering
      console.error('SDK not initialized!');
      break;
    default:
      Alert.alert('Error', error.message);
  }
}, [error]);
```

***

## Retry Behaviour

The SDK's API client automatically retries **server errors (5xx) and network timeouts** with exponential backoff:

| Attempt           | Delay          |
| ----------------- | -------------- |
| 1st retry         | 1 second       |
| 2nd retry         | 2 seconds      |
| 3rd retry (final) | — throws error |

**Client errors (4xx) are never retried** — they indicate a problem with the request itself.

***

## Error States in Hooks

All hooks expose an `error` property and a `clearError` (or `reset`) method. Prefer handling errors via state rather than try/catch at the call site:

```tsx theme={null}
const { scanPaymentRequest, error, clearError, isReading } = useNFCCustomer();

// In JSX:
{error && (
  <View>
    <Text>{error.message}</Text>
    <Button title="Try Again" onPress={() => { clearError(); scanPaymentRequest(); }} />
  </View>
)}
```

***

## Common Issues

| Symptom                                      | Likely cause                                  | Fix                                                                                                                                  |
| -------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `NFCErrorCode.NOT_SUPPORTED`                 | Simulator or non-NFC device                   | Test on a physical device with NFC                                                                                                   |
| `NFCErrorCode.TIMEOUT`                       | Device too far from terminal                  | Hold devices back-to-back and ensure HCE is active                                                                                   |
| `NFCErrorCode.INVALID_DATA` + "expired"      | Invoice older than 5 minutes                  | Have merchant create a new payment request                                                                                           |
| `NFCErrorCode.INVALID_DATA` + "signature"    | Merchant device not registered                | Call device registration before emitting HCE                                                                                         |
| `APIErrorCode.UNAUTHORIZED` (401)            | Wrong or missing API key                      | Verify `apiKey` in `initialize()`                                                                                                    |
| `APIErrorCode.SDK_NOT_INITIALIZED`           | Hook used before `initialize()`               | Move `initialize()` to app entry, before rendering                                                                                   |
| Web fallback shows "Invalid signature"       | Signature corrupted in URL                    | Ensure SDK version is up to date — earlier builds used Base64 instead of Hex                                                         |
| Web fallback shows "Payment URL has expired" | URL opened more than 35 minutes after signing | Customer must tap again to get a fresh URL; payment expiry is 30 minutes                                                             |
| iOS customer's browser doesn't open          | NFC background tag reading not enabled        | Requires iOS 14+ with background NFC reading; user may need to tap from the Control Centre NFC icon on older iOS                     |
| NDEF tap does nothing on Android             | Google Pay conflict                           | Ensure the SDK's NDEF service is declared with `category="other"` in `AndroidManifest.xml` — this prevents the disambiguation dialog |
