Skip to main content
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.
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

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.
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

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:
AttemptDelay
1st retry1 second
2nd retry2 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:
const { scanPaymentRequest, error, clearError, isReading } = useNFCCustomer();

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

Common Issues

SymptomLikely causeFix
NFCErrorCode.NOT_SUPPORTEDSimulator or non-NFC deviceTest on a physical device with NFC
NFCErrorCode.TIMEOUTDevice too far from terminalHold devices back-to-back and ensure HCE is active
NFCErrorCode.INVALID_DATA + “expired”Invoice older than 5 minutesHave merchant create a new payment request
NFCErrorCode.INVALID_DATA + “signature”Merchant device not registeredCall device registration before emitting HCE
APIErrorCode.UNAUTHORIZED (401)Wrong or missing API keyVerify apiKey in initialize()
APIErrorCode.SDK_NOT_INITIALIZEDHook used before initialize()Move initialize() to app entry, before rendering