Skip to content

Commit 3b43d6f

Browse files
committed
--
1 parent 7b34ebc commit 3b43d6f

7 files changed

Lines changed: 208 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [1.10.6](https://github.com/alexasomba/paystack-node/compare/v1.10.5...v1.10.6) (2026-06-01)
4+
5+
### Bug Fixes
6+
7+
- preserve Paystack error envelope fields on `PaystackError`
8+
- keep raw error bodies and transport causes available for diagnostics
9+
- align generated SDK release-readiness metadata checks
10+
311
## [1.9.0](https://github.com/alexasomba/paystack-node/compare/v1.8.0...v1.9.0) (2026-04-16)
412

513
### Features

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ TypeScript-first Paystack API client for Node.js, generated from the official Pa
1313
- **Smart Retries**: Automatic retries for transient failures with exponential backoff and jitter.
1414
- **Retry-After Compliance**: Automatically respects Paystack `Retry-After` headers on rate limit responses.
1515
- **Sophisticated Idempotency**: Built-in support for manual, static, or automatic UUID-based idempotency keys on POST requests.
16-
- **Detailed Error Handling**: `PaystackApiError` includes `status`, `url`, and Paystack `requestId`.
16+
- **Detailed Error Handling**: `PaystackError` preserves Paystack `code`, `type`, `meta`, request ID, HTTP status, and the raw response body.
1717
- **Webhook Verification**: Timing-safe webhook signature verification helper included.
1818

1919
## Install
@@ -61,7 +61,7 @@ const data = assertOk(result);
6161
console.log(data.authorization_url);
6262
```
6363

64-
`assertOk` returns the successful Paystack payload and throws a structured `PaystackApiError` for non-2xx responses.
64+
`assertOk` returns the successful Paystack payload and throws a structured `PaystackError` for non-2xx responses or `{ status: false }` envelopes.
6565

6666
## API Basics
6767

@@ -200,11 +200,14 @@ const error = toPaystackApiError(result);
200200

201201
if (error) {
202202
console.error(`Status ${error.status}: ${error.message}`);
203+
console.error(`Paystack code: ${error.code}`);
204+
console.error(`Paystack type: ${error.type}`);
203205
console.error(`Paystack Request ID: ${error.requestId}`);
206+
console.error(error.raw);
204207
}
205208
```
206209

207-
The `requestId` is useful when correlating logs or escalating an issue with Paystack support.
210+
Use `error.code` and `error.type` for branching on validation, processor, and API failures. The `requestId` is useful when correlating logs or escalating an issue with Paystack support, while `error.raw` / `error.body` keeps the original response envelope available for diagnostics.
208211

209212
## Errors
210213

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"lint": "vp lint . --fix",
7575
"lint:package": "publint run --strict",
7676
"lint:skills": "intent validate",
77-
"lint:types": "npm_config_cache=/private/tmp/paystack-openapi-npm-cache attw --profile esm-only --pack .",
77+
"lint:types": "npm_config_ignore_scripts=true npm_config_cache=/tmp/paystack-openapi-npm-cache attw --profile esm-only --pack .",
7878
"typecheck": "tsc -p tsconfig.json --noEmit",
7979
"clean": "rm -rf dist",
8080
"prepack": "pnpm build",

src/errors.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ export function getPaystackRequestId(headers: Headers | HeadersInit): string | u
1414
return undefined;
1515
}
1616

17+
export type PaystackErrorType =
18+
| "api_error"
19+
| "validation_error"
20+
| "processor_error"
21+
| (string & {});
22+
23+
export interface PaystackErrorOptions {
24+
message: string;
25+
code?: string;
26+
type?: PaystackErrorType;
27+
status?: number;
28+
requestId?: string;
29+
meta?: Record<string, unknown>;
30+
/** Parsed Paystack response envelope or transport payload used to create this error. */
31+
raw?: unknown;
32+
/** Alias for raw, kept for callers that prefer body-oriented naming. */
33+
body?: unknown;
34+
/** Original transport/openapi-fetch error, when one exists. */
35+
cause?: unknown;
36+
}
37+
1738
/**
1839
* Standard Paystack API Error
1940
*/
@@ -28,27 +49,29 @@ export class PaystackError extends Error {
2849
public readonly requestId?: string;
2950
/** Additional metadata from the error response */
3051
public readonly meta?: Record<string, unknown>;
52+
/** Parsed Paystack response envelope or transport payload used to create this error. */
53+
public readonly raw?: unknown;
54+
/** Alias for raw, kept for callers that prefer body-oriented naming. */
55+
public readonly body?: unknown;
3156

32-
constructor(options: {
33-
message: string;
34-
code?: string;
35-
type?: string;
36-
status?: number;
37-
requestId?: string;
38-
meta?: Record<string, unknown>;
39-
}) {
57+
constructor(options: PaystackErrorOptions) {
4058
const requestId = options.requestId;
4159
const suffix =
4260
requestId !== undefined && requestId !== null && requestId !== ""
4361
? ` (requestId: ${requestId})`
4462
: "";
45-
super(`${options.message}${suffix}`);
63+
super(
64+
`${options.message}${suffix}`,
65+
options.cause === undefined ? undefined : { cause: options.cause },
66+
);
4667
this.name = "PaystackError";
4768
this.code = options.code;
4869
this.type = options.type;
4970
this.status = options.status;
5071
this.requestId = options.requestId;
5172
this.meta = options.meta;
73+
this.raw = options.raw ?? options.body;
74+
this.body = options.body ?? options.raw;
5275

5376
// Ensure proper stack trace in Node.js
5477
if (typeof Error.captureStackTrace === "function") {
@@ -71,3 +94,15 @@ export class PaystackError extends Error {
7194
return this.type === "validation_error";
7295
}
7396
}
97+
98+
/**
99+
* @deprecated Use PaystackError
100+
*/
101+
export const PaystackApiError = PaystackError;
102+
103+
/**
104+
* @deprecated Use PaystackError
105+
*/
106+
export function isPaystackApiError(value: unknown): value is PaystackError {
107+
return value instanceof PaystackError;
108+
}

src/response.ts

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PaystackError, getPaystackRequestId } from "./errors.js";
1+
import { PaystackError, getPaystackRequestId, type PaystackErrorType } from "./errors.js";
22

33
/**
44
* Base structure of a Paystack API response body
@@ -7,7 +7,53 @@ export interface PaystackRawResponse<T = unknown> {
77
status: boolean;
88
message: string;
99
data: T;
10+
code?: string;
11+
type?: PaystackErrorType;
1012
meta?: Record<string, unknown>;
13+
errorCodeMappingNotFound?: boolean;
14+
}
15+
16+
interface PaystackErrorEnvelope {
17+
message?: string;
18+
code?: string;
19+
type?: PaystackErrorType;
20+
meta?: Record<string, unknown>;
21+
}
22+
23+
function isRecord(value: unknown): value is Record<string, unknown> {
24+
return typeof value === "object" && value !== null;
25+
}
26+
27+
function getStringField(source: unknown, field: string): string | undefined {
28+
if (!isRecord(source)) return undefined;
29+
const value = source[field];
30+
return typeof value === "string" && value !== "" ? value : undefined;
31+
}
32+
33+
function getMeta(source: unknown): Record<string, unknown> | undefined {
34+
if (!isRecord(source)) return undefined;
35+
const value = source.meta;
36+
return isRecord(value) ? value : undefined;
37+
}
38+
39+
function getPaystackEnvelope(source: unknown): PaystackErrorEnvelope {
40+
return {
41+
message: getStringField(source, "message"),
42+
code: getStringField(source, "code"),
43+
type: getStringField(source, "type") as PaystackErrorType | undefined,
44+
meta: getMeta(source),
45+
};
46+
}
47+
48+
function resolveErrorMessage(error: unknown, raw: unknown): string {
49+
const errorEnvelope = getPaystackEnvelope(error);
50+
if (errorEnvelope.message !== undefined) return errorEnvelope.message;
51+
52+
const rawEnvelope = getPaystackEnvelope(raw);
53+
if (rawEnvelope.message !== undefined) return rawEnvelope.message;
54+
55+
if (error instanceof Error && error.message !== "") return error.message;
56+
return "Network or HTTP Error";
1157
}
1258

1359
/**
@@ -48,19 +94,19 @@ export class PaystackResponse<T> {
4894
const requestId = getPaystackRequestId(this.response.headers);
4995

5096
if (this.error !== undefined && this.error !== null) {
51-
// Handle HTTP or Network errors
52-
let message = "Network or HTTP Error";
53-
if (typeof this.error === "object" && this.error !== null && "message" in this.error) {
54-
message = String((this.error as { message: unknown }).message);
55-
} else if (this.raw && typeof this.raw === "object" && "message" in this.raw) {
56-
message = this.raw.message;
57-
}
97+
const errorEnvelope = getPaystackEnvelope(this.error);
98+
const rawEnvelope = getPaystackEnvelope(this.raw);
5899

59100
throw new PaystackError({
60-
message,
101+
message: resolveErrorMessage(this.error, this.raw),
102+
code: errorEnvelope.code ?? rawEnvelope.code,
103+
type: errorEnvelope.type ?? rawEnvelope.type,
61104
status: this.response.status,
62105
requestId,
63-
meta: this.raw?.meta,
106+
meta: errorEnvelope.meta ?? rawEnvelope.meta,
107+
raw: this.error,
108+
body: this.error,
109+
cause: this.error,
64110
});
65111
}
66112

@@ -70,16 +116,24 @@ export class PaystackResponse<T> {
70116
message: "Empty response body",
71117
status: this.response.status,
72118
requestId,
119+
raw: this.raw,
120+
body: this.raw,
73121
});
74122
}
75123

76124
if (!this.raw.status) {
125+
const rawEnvelope = getPaystackEnvelope(this.raw);
126+
77127
// Handle Paystack-level errors (status: false)
78128
throw new PaystackError({
79-
message: this.raw.message,
129+
message: rawEnvelope.message ?? "Paystack API Error",
130+
code: rawEnvelope.code,
131+
type: rawEnvelope.type,
80132
status: this.response.status,
81133
requestId,
82-
meta: this.raw.meta,
134+
meta: rawEnvelope.meta,
135+
raw: this.raw,
136+
body: this.raw,
83137
});
84138
}
85139

@@ -93,9 +147,20 @@ export class PaystackResponse<T> {
93147
* @throws {PaystackError} if status is false.
94148
*/
95149
public get data(): T {
96-
if (this.raw === undefined || this.raw === null) {
97-
return this.unwrap();
98-
}
99-
return this.raw.data;
150+
return this.unwrap();
151+
}
152+
}
153+
154+
export function assertOk<T>(result: PaystackResponse<T>): T {
155+
return result.unwrap();
156+
}
157+
158+
export function toPaystackApiError<T>(result: PaystackResponse<T>): PaystackError | undefined {
159+
try {
160+
result.unwrap();
161+
return undefined;
162+
} catch (error) {
163+
if (error instanceof PaystackError) return error;
164+
throw error;
100165
}
101166
}

test/errors.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vite-plus/test";
2-
import { PaystackError, getPaystackRequestId } from "../src/errors.js";
2+
import { PaystackError, getPaystackRequestId, isPaystackApiError } from "../src/errors.js";
33

44
describe("Error Handling", () => {
55
describe("getPaystackRequestId", () => {
@@ -22,13 +22,22 @@ describe("Error Handling", () => {
2222

2323
describe("PaystackError", () => {
2424
it("should correctly capture error details", () => {
25+
const raw = {
26+
status: false,
27+
message: "Failed",
28+
code: "invalid_params",
29+
type: "validation_error",
30+
};
31+
const cause = new Error("Original transport error");
2532
const error = new PaystackError({
2633
message: "Failed",
2734
status: 400,
2835
requestId: "req_123",
2936
code: "invalid_params",
3037
type: "validation_error",
3138
meta: { field: "email" },
39+
raw,
40+
cause,
3241
});
3342

3443
expect(error.message).toContain("Failed");
@@ -38,6 +47,10 @@ describe("Error Handling", () => {
3847
expect(error.code).toBe("invalid_params");
3948
expect(error.type).toBe("validation_error");
4049
expect(error.meta).toEqual({ field: "email" });
50+
expect(error.raw).toBe(raw);
51+
expect(error.body).toBe(raw);
52+
expect(error.cause).toBe(cause);
53+
expect(isPaystackApiError(error)).toBe(true);
4154
});
4255

4356
it("should identify validation errors", () => {

0 commit comments

Comments
 (0)