Nylon PayNylon Pay

Error Handling

Error reasons, parseError, throw vs. result, retries, and timeouts

Error Reasons

All SDK errors carry a reason, a machine-readable label you branch on instead of parsing HTTP codes or message text.

type SdkErrorReason =
  | "AUTH"          // invalid or missing key, bad signature, scope
  | "VALIDATION"    // input the server rejected
  | "LIMIT"         // account or KYC transaction limits exceeded
  | "RATE_LIMIT"    // too many requests
  | "ACCOUNT"       // merchant account missing or not active
  | "PROVIDER"      // payment provider rejected the operation
  | "DUPLICATE"     // reference belongs to another account; retry with a new one
  | "NOT_FOUND"     // referenced transaction does not exist
  | "INTERNAL"      // unexpected server error
  | "NETWORK"       // this machine is offline
  | "SERVICES_DOWN" // Nylon Pay did not complete the request
  | "TIMEOUT";      // request exceeded the configured timeout

Each reason maps to a retryable hint:

ReasonRetryableMeaning
AUTHNoFix credentials, then retry
VALIDATIONNoCorrect the input, then retry
LIMITNoContact support about account limits
RATE_LIMITYesBack off and retry
ACCOUNTNoContact support about account status
PROVIDERYesRetry after a delay
DUPLICATENoUse a new reference
NOT_FOUNDNoTransaction or reference does not exist
INTERNALYesRetry after a delay
NETWORKYesRestore this machine's connection
SERVICES_DOWNYesRetry when Nylon Pay recovers
TIMEOUTYesRetry the request

Offline and Nylon down

Pass onError when you create the SDK client. It handles errors from every operation on the client, including offline calls and calls while Nylon Pay looks down.

import { createNylonPay } from '@nile-squad/nylonpay-ts';

const nylonpay = createNylonPay({
  apiKey: 'npk_test_...',
  apiSecret: 'nps_test_...',
  onError: (error) => {
    if (error.reason === 'SERVICES_DOWN') {
      console.error('Pause payment attempts:', error.message);
      return;
    }
    console.error(error.reason, error.message);
  },
});

NETWORK means this machine is offline. SERVICES_DOWN means Nylon Pay did not complete the request. Compare the reason, not message text. The same error reaches payment.on("error") for collectPayment and makePayout. Result operations return the error for parseError or parse_error.

Pause the operation and retry when the connection or Nylon Pay service recovers.

Handler failures do not change the operation result. Validation errors raised before a network call remain local throws.

parseError Utility

parseError decodes the error string from a Result.Err into a structured SdkError object.

import { parseError } from '@nile-squad/nylonpay-ts';

const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });

if (!result.isOk) {
  const error = parseError(result.error);
  console.log(error.reason);    // "AUTH" | "NOT_FOUND" | "LIMIT" | ...
  console.log(error.message);   // Human-readable description
  console.log(error.retryable); // Whether retrying may help
}

Branch on error.reason, never on message text or HTTP status codes. Unrecognized errors default to INTERNAL.

Throw vs. Result

The SDK uses two error patterns depending on the operation.

Operations that throw

collectPayment() and makePayout() throw only on client-side validation errors (zero amount, empty required fields, invalid items, missing bank details). These are programmer errors caught before any network call.

try {
  const payment = await nylonpay.collectPayment({ /* ... */ });
  payment.on('success', ({ transaction }) => fulfill(transaction));
} catch (err) {
  // err.reason === "VALIDATION"
  console.error('Invalid input:', err.message);
}

Server-side initiation failures

If a transaction fails to start (invalid key, scope or limit rejection, provider rejection, network error, timeout), collectPayment and makePayout return a PaymentInstance emitting an "error" event. The transaction never started, so there is nothing to poll.

const payment = await nylonpay.collectPayment({ /* ... */ });

payment.on('error', ({ error, reason, retryable }) => {
  console.error('Could not start payment:', error, 'reason:', reason);
});

payment.on('success', ({ transaction }) => fulfill(transaction));

Operations returning Result

All other operations (getStatus, getTransaction, verifyPhone, createInvoice, collectPaymentAndResolve, makePayoutAndResolve) return a Result. Decode the Err with parseError.

const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });

if (result.isOk) {
  console.log(result.value.status);
} else {
  const error = parseError(result.error);
  console.error(error.reason, error.message);
}

PaymentInstance error event

Errors during the polling lifecycle (network failures, timeouts, reference mismatches) and server-side initiation failures (auth, limit, provider rejection) surface through the "error" event, not exceptions. The EventData carries reason and retryable for programmatic handling.

payment.on('error', ({ error, reason, retryable }) => {
  console.error('Payment lifecycle error:', error, 'reason:', reason, 'retryable:', retryable);
});

Result Pattern

All SDK methods returning Result follow this shape:

type Result<T, E> = { isOk: true; value: T } | { isOk: false; error: E };

Always check isOk before accessing value:

const result = await nylonpay.createInvoice({ /* ... */ });

if (result.isOk) {
  console.log('Invoice URL:', result.value.paymentLink);
} else {
  const error = parseError(result.error);
  console.error(error.reason, error.message);
}

Retry Behavior

Automatic Retries

The SDK retries failed HTTP requests automatically:

  • Transport retries: Up to maxRetries attempts for network failures
  • Business errors are not retried: client errors return immediately
const nylonpay = createNylonPay({
  maxRetries: 3, // Default
});

Retry Strategy

  1. First attempt
  2. Wait
  3. Second attempt
  4. Wait
  5. Third attempt
  6. Fail with error

Timeout Behavior

TimeoutConfigDefaultApplies To
Per-requesttimeoutMsabout 90 secSingle HTTP request
Poll durationmaxPollDurationMsnoneOptional cap on the total wait() time
Poll attemptsmaxPollAttemptsnoneOptional cap on wait() checks
const nylonpay = createNylonPay({
  timeoutMs: 90000, // about 90 seconds
  maxPollDurationMs: 300000,
  maxPollAttempts: 150,
});

Best Practices

Always Use Reference for Idempotency

// Good: Fresh UUID each payment
const reference = crypto.randomUUID();
const payment = await nylonpay.collectPayment({ reference, ... });

// Bad: Hardcoded reference
const payment = await nylonpay.collectPayment({ reference: 'order-1', ... });

Catch Initiation Errors Separately

const payment = await nylonpay.collectPayment({ /* ... */ });

payment.on('error', ({ error, reason, retryable }) => {
  if (reason === 'AUTH') {
    // Fix credentials
  } else if (reason === 'LIMIT') {
    // Contact support about account limits
  } else if (retryable) {
    // Retry after delay
  }
});

Verify After Timeout

const tx = await payment.wait();
if (tx) {
  fulfillOrder(tx);
} else {
  const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });
  if (result.isOk && result.value.status === 'successful') {
    fulfillOrder();
  }
}

Never Ignore Errors

const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });
if (!result.isOk) {
  const error = parseError(result.error);
  throw new Error(`Status check failed: [${error.reason}] ${error.message}`);
}

See Also

On this page