Error handling
The error-first result tuple, how to narrow on error.type, and when each error subclass is returned.
Every method on WompiClient and WompiPayoutsClient returns a Result<T>: a discriminated tuple of either [error, null] or [null, data]. This makes failure a value, not a thrown exception, and narrows the types automatically.
The tuple
import { WompiClient } from "@pulgueta/wompi";
const wompi = new WompiClient({
publicKey: process.env.WOMPI_PUBLIC_KEY!,
sandbox: true,
});
const [error, response] =
await wompi.transactions.getTransaction("txn_does_not_exist");
if (error) {
// Inside this branch, response is typed as null.
return;
}
// And inside this branch, response is fully typed.
response.status;
Error shapes
Every SDK error extends the base WompiError. The response source determines
which subclass you receive:
| Class | Discriminant | Returned when |
|---|---|---|
WompiError |
— | Local validation, a missing key, a different pre-request failure, or a 2xx response that does not agree with the expected schema. |
WompiValidationError |
type === "INPUT_VALIDATION_ERROR" |
An HTTP 422 response whose body is a Payments API validation error. |
WompiNotFoundError |
type === "NOT_FOUND_ERROR" |
An HTTP 404 response whose body is a Payments API not-found error. |
WompiRequestError |
statusCode and body |
A network failure or a timeout (statusCode is 0), a 2xx response that is not JSON, or a different non-2xx response that is not a structured Payouts API error. |
WompiServiceUnavailableError |
type === "SERVICE_UNAVAILABLE_ERROR"; retryable === true |
Wompi or its gateway answers 502, 503 or 504, or a 5xx that is not JSON, and the response is not a structured Payouts API error. |
WompiPayoutApiError |
type === "PAYOUT_API_ERROR"; code, statusCode, and body |
The Payouts API returns a structured error. |
WompiWebhookVerificationError |
type === "WEBHOOK_VERIFICATION_ERROR" |
A payment or payout webhook fails parsing or signature verification. |
Branching on type
The simplest pattern is to switch on the discriminant — no class import needed.
if (error) {
if ("type" in error && error.type === "NOT_FOUND_ERROR") {
return ctx.text("Transaction not found", 404);
}
if ("type" in error && error.type === "INPUT_VALIDATION_ERROR") {
return ctx.json({ messages: error.messages }, 400);
}
if ("type" in error && error.type === "PAYOUT_API_ERROR") {
return ctx.json({ statusCode: error.statusCode, code: error.code }, 502);
}
if ("type" in error && error.type === "SERVICE_UNAVAILABLE_ERROR") {
// Wompi is not available at this time. Try again after a delay.
return ctx.text("Payment provider unavailable", 503);
}
if ("statusCode" in error && "body" in error) {
// WompiRequestError exposes the raw response as body.
return ctx.json({ statusCode: error.statusCode, body: error.body }, 502);
}
// Generic WompiError — usually means a bad payload before we sent it.
return ctx.text(error.message, 422);
}
Or with instanceof
If you prefer class-based branching, import the error classes from the /schemas subpath.
import {
WompiError,
WompiNotFoundError,
WompiPayoutApiError,
WompiRequestError,
WompiServiceUnavailableError,
WompiValidationError,
WompiWebhookVerificationError,
} from "@pulgueta/wompi/schemas";
if (error instanceof WompiValidationError) {
// error.messages is Record<string, string[]>
}
if (error instanceof WompiNotFoundError) {
// error.reason is the human-readable reason string
}
if (error instanceof WompiPayoutApiError) {
// error.code and error.statusCode identify it; error.body keeps diagnostics
}
if (error instanceof WompiServiceUnavailableError) {
// error.statusCode and error.retryable; do this check before WompiRequestError
}
if (error instanceof WompiRequestError) {
// error.statusCode and error.body
}
if (error instanceof WompiWebhookVerificationError) {
// Reject the webhook without processing its payload
}
if (error instanceof WompiError) {
// base class — every error above is also a WompiError
}
Gateway and availability errors
When Wompi or a gateway in front of it cannot serve a request, the response is a 502, 503 or 504, frequently with an HTML page as the body. The SDK returns a WompiServiceUnavailableError for these responses, and for each other 5xx response that is not JSON.
This error is different from a rejection. The request did not fail because of its content, so the correct action is to try again after a delay. Do not record it as a declined payment.
import { isGatewayError } from "@pulgueta/wompi/schemas";
const [error, payout] = await payouts.createPayout(input, { idempotencyKey });
if (error && isGatewayError(error)) {
// Try again with the same idempotency key, before the key expires.
return scheduleRetry();
}
isGatewayError(error) is true for a WompiServiceUnavailableError. It is also true for a WompiRequestError or a WompiPayoutApiError that has a 502, 503 or 504 status code.
WompiServiceUnavailableError extends WompiRequestError. Code that reads statusCode from a WompiRequestError continues to operate. The body is null when the response was not JSON.
Pre-flight input validation
Every method parses its arguments with Zod before sending them. A bad input never reaches the network — you get a WompiError with a flattened message instead, listing each bad field.
const [error] = await wompi.transactions.createTransaction({
// amount_in_cents missing on purpose
acceptance_token: "...",
currency: "COP",
signature: "...",
customer_email: "buyer@example.com",
reference: "ref-1",
payment_method: { type: "CARD", token: "tok_x", installments: 1 },
});
// error.message:
// "Invalid input: amount_in_cents: Required"
The SDK never throws (almost)
Three exceptions:
new WompiClient(options)throws if the options fail Zod validation — usually missing or emptypublicKey.new WompiPayoutsClient(options)throws if its API key or user principal ID fails Zod validation.getSignatureKeyfrom/serverthrows ifamountInCentsisn’t a non-negative integer.
Everywhere else, failure travels through the tuple.