Nylon PayNylon Pay

Error Handling

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

Error Categories

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

type SdkErrorCategory =
  | "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
  | "not_found"    // referenced transaction does not exist
  | "internal"     // unexpected server error
  | "network"      // request never reached the server
  | "timeout";     // request exceeded the configured timeout

Each category maps to a retryable hint:

CategoryRetryableMeaning
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
not_foundNoTransaction or reference does not exist
internalYesRetry after a delay
networkYesRetry the request
timeoutYesRetry the request

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.category);  // "auth" | "not_found" | "limit" | ...
  console.log(error.message);   // Human-readable description
  console.log(error.retryable); // Whether retrying may help
}

Branch on error.category, never on message text or HTTP status codes. Unrecognized errors default to category 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.category === "validation"
  console.error('Invalid input:', err.message);
}

Server-side initiation failures

If the transaction fails to start on the server (invalid key, bad signature, scope/limit rejection, provider rejection, network error, timeout), collectPayment and makePayout return a PaymentInstance that emits an "error" event. The transaction never started, so there is nothing to poll.

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

payment.on('error', ({ error, category, retryable }) => {
  // category: "auth" | "limit" | "provider" | "network" | "timeout" | ...
  // retryable: true for provider/network/timeout, false for auth/limit
  console.error('Could not start payment:', error, 'category:', category);
});

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.category, 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 category and retryable for programmatic handling.

payment.on('error', ({ error, category, retryable }) => {
  console.error('Payment lifecycle error:', error, 'category:', category, '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.category, 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-requesttimeoutMs30sSingle HTTP request
Poll durationmaxPollDurationMs5 minwait() total time
Poll attemptsmaxPollAttempts150wait() max polls
const nylonpay = createNylonPay({
  timeoutMs: 30000,
  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({ /* ... */ });

// Server-side initiation failures (auth, limit, provider, network, timeout)
payment.on('error', ({ error, category, retryable }) => {
  if (category === 'auth') {
    // Fix credentials
  } else if (category === 'limit') {
    // Contact support about account limits
  } else if (retryable) {
    // Retry after delay for provider/network/timeout
  }
});

// Client-side validation errors (zero amount, empty fields) throw synchronously
// and are caught by the try/catch around the call if you wrap it

Verify After Timeout

const tx = await payment.wait();
if (tx) {
  fulfillOrder(tx);
} else {
  // Payment did not succeed, verify actual status
  const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });
  if (result.isOk && result.value.status === 'successful') {
    fulfillOrder();
  }
}

Never Ignore Errors

// Bad
const result = await nylonpay.getStatus({ reference: crypto.randomUUID() });
console.log(result.value.status); // TypeError if isOk is false

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

On this page