Error Handling
Sanwo surfaces errors through the CheckoutResult when a checkout completes, and through the SanwoErrorInfo object that gives you structured details about what went wrong and whether you can retry.
The checkout result
Section titled “The checkout result”Every checkout returns a result with a status field. The "failed" status is the primary error path:
type CheckoutResult = | { status: "successful"; reference: string; providerResponse: unknown } | { status: "cancelled" } | { status: "failed"; error: SanwoErrorInfo } | { status: "pending"; reference: string };When status is "failed", the error field contains a SanwoErrorInfo object with everything you need to diagnose and respond to the failure.
SanwoErrorInfo
Section titled “SanwoErrorInfo”interface SanwoErrorInfo { code: SanwoErrorCode; // Machine-readable error code message: string; // Human-readable description provider?: string; // Which provider reported the error (if applicable) recoverable: boolean; // Can you retry the checkout? cause?: unknown; // The underlying error (provider SDK error, network error, etc.)}The code field is the key to programmatic error handling. The message is suitable for logging but may be too technical for end users – write your own user-facing messages based on the code.
Error codes
Section titled “Error codes”Sanwo defines 12 error codes. Each one tells you what went wrong and what to do about it.
Configuration errors
Section titled “Configuration errors”These indicate a problem with how Sanwo was set up. They are not recoverable – fix the configuration and redeploy.
| Code | Description | What to do |
|---|---|---|
INVALID_CONFIGURATION |
The Sanwo instance was created with invalid config | Check that provider and publicKey are correct. Make sure you imported the right provider package. |
INVALID_CHECKOUT_OPTIONS |
The checkout options are invalid | Verify amount is a positive number, currency is a valid ISO 4217 code, and customer.email is present. |
UNSUPPORTED_CURRENCY |
The active provider does not support the requested currency | Switch to a provider that supports this currency, or use a different currency. See Choosing a Provider. |
UNSUPPORTED_CAPABILITY |
A requested feature is not available for this provider | The provider does not support a feature you are trying to use. Remove the unsupported option or switch providers. |
Runtime errors
Section titled “Runtime errors”These can happen during a checkout. Some are recoverable.
| Code | Description | What to do |
|---|---|---|
PROVIDER_LOAD_FAILED |
The provider SDK could not be loaded | Usually a network or CDN issue. Check connectivity and retry. |
PROVIDER_NOT_READY |
The provider has not finished initializing | Wait for initialization to complete before calling checkout. This can happen if you call checkout immediately after creating the instance. |
SCRIPT_LOAD_FAILED |
The provider’s JavaScript SDK failed to load | The <script> tag for the provider’s checkout JS could not load. Check for ad blockers, CSP issues, or network problems. |
CHECKOUT_ALREADY_ACTIVE |
Another checkout is already in progress | Wait for the current checkout to complete before starting a new one. |
CHECKOUT_FAILED |
The payment was declined or failed at the provider level | The provider rejected the payment. The message field usually contains the reason (e.g., insufficient funds, card declined). Show the user an appropriate message. |
CHECKOUT_CANCELLED |
The user closed the checkout | This is also surfaced via result.status === "cancelled". Prefer checking the status field directly. |
NETWORK_ERROR |
A connectivity issue occurred during checkout | Check the user’s internet connection and retry. |
TIMEOUT |
The checkout exceeded the configured timeout | The default timeout is 120 seconds. If your checkout flow needs more time, increase the timeout in your configuration. |
Handling errors by platform
Section titled “Handling errors by platform”const result = await sanwo({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" },});
switch (result.status) { case "successful": console.log("Paid!", result.reference); break;
case "failed": console.error(result.error.code, result.error.message);
if (result.error.recoverable) { // Safe to retry showRetryButton(); } else { // Fix configuration and redeploy showSupportContact(); } break;
case "cancelled": console.log("User closed checkout"); break;
case "pending": console.log("Verify server-side:", result.reference); break;}function CheckoutPage() { const { checkout, isLoading, error } = useSanwoCheckout();
async function pay() { const result = await checkout({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" }, });
if (result.status === "failed") { console.error(result.error.code, result.error.message); } }
// The error state also catches setup errors (e.g., provider failed to load) if (error) { return <p>Something went wrong: {error.message}</p>; }
return ( <button onClick={pay} disabled={isLoading}> {isLoading ? "Processing..." : "Pay"} </button> );}The error value from useSanwoCheckout() catches errors that happen outside the normal checkout flow – for example, if the provider SDK fails to load during initialization. Always check both the hook’s error state and the result’s status.
final result = await sanwo( context: context, options: CheckoutOptions( amount: 500000, currency: 'NGN', customer: CheckoutCustomer(email: 'customer@example.com'), ),);
switch (result.status) { case CheckoutStatus.successful: print('Paid! Ref: ${result.reference}'); break; case CheckoutStatus.failed: print('Error: ${result.error}'); // Check if retry is possible if (result.recoverable) { showRetryDialog(context); } break; case CheckoutStatus.cancelled: print('User cancelled'); break;}sanwo(from: self, options: options) { result in switch result { case .successful(let data): print("Paid! Ref: \(data.reference)")
case .failed(let error): print("Error: \(error.code) - \(error.message)") if error.recoverable { self.showRetryAlert() }
case .cancelled: print("User cancelled") }}val launcher = sanwo.registerForCheckoutResult(this) { result -> when (result) { is CheckoutResult.Successful -> { Log.d("Sanwo", "Paid! Ref: ${result.reference}") } is CheckoutResult.Failed -> { Log.e("Sanwo", "${result.error.code}: ${result.error.message}") if (result.error.recoverable) { showRetrySnackbar() } } is CheckoutResult.Cancelled -> { Log.d("Sanwo", "User cancelled") } }}The recoverable flag
Section titled “The recoverable flag”Every SanwoErrorInfo includes a recoverable boolean that tells you whether retrying the checkout is likely to help.
Recoverable errors (recoverable: true):
PROVIDER_LOAD_FAILED– network issue loading the SDK, try againNETWORK_ERROR– connectivity problem, may resolveCHECKOUT_FAILED– payment declined, user can try a different cardTIMEOUT– checkout took too long, user can try againSCRIPT_LOAD_FAILED– script blocked or network issue, may work on retry
Non-recoverable errors (recoverable: false):
INVALID_CONFIGURATION– code change requiredINVALID_CHECKOUT_OPTIONS– fix the options being passedUNSUPPORTED_CURRENCY– wrong provider for this currencyUNSUPPORTED_CAPABILITY– feature not availablePROVIDER_NOT_READY– initialization timing issueCHECKOUT_ALREADY_ACTIVE– wait for the current checkout
Use this flag to decide whether to show a “Try again” button or a “Contact support” message.
Timeout configuration
Section titled “Timeout configuration”The default checkout timeout is 120 seconds (2 minutes). If a checkout is not completed within this window, it fails with the TIMEOUT error code.
You can configure a longer timeout when creating the Sanwo instance:
const sanwo = createSanwo({ provider: paystackProvider, publicKey: "pk_test_xxxxxxxxxxxx", timeout: 180000, // 3 minutes in milliseconds});<SanwoReactProvider provider={paystackProvider} publicKey="pk_test_xxxxxxxxxxxx" timeout={180} // 3 minutes in seconds> {children}</SanwoReactProvider>final sanwo = Sanwo( provider: paystackProvider, publicKey: 'pk_test_xxxxxxxxxxxx', timeout: 180, // 3 minutes in seconds);Consider increasing the timeout for:
- Markets where bank transfer confirmation takes longer
- Checkout flows that require OTP or 3D Secure verification
- Slower network environments
The onError callback
Section titled “The onError callback”The CheckoutResult captures the final outcome of a checkout. But sometimes non-fatal errors happen during a checkout – for example, a provider script that needs retrying internally.
Use the onError callback in your checkout options to observe these transient errors:
const result = await sanwo({ amount: 500000, currency: "NGN", customer: { email: "customer@example.com" }, onError: (error) => { // Non-fatal error during checkout console.warn("Checkout warning:", error.code, error.message); analytics.track("checkout_warning", { code: error.code }); },});The onError callback fires for issues that Sanwo handles internally (like retrying a script load). The checkout may still succeed. Use it for logging and monitoring, not for control flow.
Debug mode
Section titled “Debug mode”Enable debug mode to log all internal Sanwo events to the console. This is invaluable when diagnosing checkout issues during development.
const sanwo = createSanwo({ provider: paystackProvider, publicKey: "pk_test_xxxxxxxxxxxx", debug: true,});<SanwoReactProvider provider={paystackProvider} publicKey="pk_test_xxxxxxxxxxxx" debug={true}> {children}</SanwoReactProvider>final sanwo = Sanwo( provider: paystackProvider, publicKey: 'pk_test_xxxxxxxxxxxx', debug: true,);Debug mode logs:
- Provider initialization steps
- Checkout lifecycle events (started, opened, loaded, closed)
- Provider SDK requests and responses
- Error details including the underlying
cause
Always disable debug mode in production.
Common patterns
Section titled “Common patterns”User-friendly error messages
Section titled “User-friendly error messages”Map error codes to messages your users will understand:
function getUserMessage(code: SanwoErrorCode): string { switch (code) { case "CHECKOUT_FAILED": return "Your payment could not be processed. Please try a different payment method."; case "NETWORK_ERROR": return "Please check your internet connection and try again."; case "TIMEOUT": return "The payment took too long. Please try again."; case "CHECKOUT_CANCELLED": return "Payment was cancelled."; default: return "Something went wrong. Please try again or contact support."; }}Retry with exponential backoff
Section titled “Retry with exponential backoff”For recoverable errors in automated flows:
async function checkoutWithRetry(options, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { const result = await sanwo(options);
if (result.status !== "failed" || !result.error.recoverable) { return result; }
if (attempt < maxRetries) { await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt))); } }
return { status: "failed", error: { code: "TIMEOUT", message: "Max retries exceeded", recoverable: false } };}Logging errors for monitoring
Section titled “Logging errors for monitoring”Track errors to catch provider issues early:
const result = await sanwo(options);
if (result.status === "failed") { errorTracker.capture("payment_failed", { code: result.error.code, message: result.error.message, provider: result.error.provider, recoverable: result.error.recoverable, });}