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 timeoutEach category maps to a retryable hint:
| Category | Retryable | Meaning |
|---|---|---|
auth | No | Fix credentials, then retry |
validation | No | Correct the input, then retry |
limit | No | Contact support about account limits |
rate_limit | Yes | Back off and retry |
account | No | Contact support about account status |
provider | Yes | Retry after a delay |
not_found | No | Transaction or reference does not exist |
internal | Yes | Retry after a delay |
network | Yes | Retry the request |
timeout | Yes | Retry 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
maxRetriesattempts for network failures - Business errors are not retried: client errors return immediately
const nylonpay = createNylonPay({
maxRetries: 3, // Default
});Retry Strategy
- First attempt
- Wait
- Second attempt
- Wait
- Third attempt
- Fail with error
Timeout Behavior
| Timeout | Config | Default | Applies To |
|---|---|---|---|
| Per-request | timeoutMs | 30s | Single HTTP request |
| Poll duration | maxPollDurationMs | 5 min | wait() total time |
| Poll attempts | maxPollAttempts | 150 | wait() 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 itVerify 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}`);
}