Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2089,3 +2089,49 @@ export class ChannelExhaustedError extends StellarSplitError {
Object.setPrototypeOf(this, new.target.prototype);
}
}

/**
* Canonical SDK error codes.
*
* Consumers can `switch` on these values instead of string-matching error
* messages, which is brittle when wording changes.
*/
export enum SdkErrorCode {
INVOICE_NOT_FOUND = "INVOICE_NOT_FOUND",
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
DEADLINE_EXPIRED = "DEADLINE_EXPIRED",
INVALID_RECIPIENT = "INVALID_RECIPIENT",
CONTRACT_REJECTED = "CONTRACT_REJECTED",
NETWORK_TIMEOUT = "NETWORK_TIMEOUT",
RATE_LIMITED = "RATE_LIMITED",
}

/**
* Typed SDK error carrying a stable machine-readable error code.
*
* Extends the native `Error` so `instanceof Error` checks and `stack`
* capture work as expected, while consumers can reliably switch on
* {@link SdkErrorCode}.
*/
export class SdkError extends Error {
/** Stable machine-readable error code. */
readonly code: SdkErrorCode;
/** Optional structured context for debugging or safe handling. */
readonly details?: unknown;

constructor(code: SdkErrorCode, message: string, details?: unknown) {
super(message);
this.name = "SdkError";
this.code = code;
this.details = details;
// Maintain proper prototype chain in transpiled environments
Object.setPrototypeOf(this, new.target.prototype);
}
}

/**
* Type guard that narrows an unknown value to {@link SdkError}.
*/
export function isSdkError(err: unknown): err is SdkError {
return err instanceof SdkError;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@ export { ContractRetryQueue } from "./contractRetryQueue.js";
export type { ContractInvocationExecutor, ContractRetryQueueConfig } from "./contractRetryQueue.js";
export type { ContractInvocation, ContractResult } from "./types.js";
export { ContractRetryExhaustedError, isContractRetryExhaustedError } from "./errors.js";
export { SdkErrorCode, SdkError, isSdkError } from "./errors.js";

// Invoice batch processor with concurrency limiter
export { InvoiceBatchProcessor } from "./invoiceBatchProcessor.js";
Expand Down
72 changes: 72 additions & 0 deletions test/sdkError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import { SdkError, SdkErrorCode, isSdkError } from "../src/errors.js";

describe("SdkError", () => {
it("carries the correct error code", () => {
const err = new SdkError(SdkErrorCode.INVOICE_NOT_FOUND, "Invoice not found");
expect(err.code).toBe(SdkErrorCode.INVOICE_NOT_FOUND);
});

it("preserves the error message", () => {
const msg = "Invoice not found: abc-123";
const err = new SdkError(SdkErrorCode.INVOICE_NOT_FOUND, msg);
expect(err.message).toBe(msg);
});

it("captures optional details", () => {
const details = { invoiceId: "abc-123" };
const err = new SdkError(SdkErrorCode.INVOICE_NOT_FOUND, "not found", details);
expect(err.details).toEqual(details);
});

it("is an instance of Error", () => {
const err = new SdkError(SdkErrorCode.NETWORK_TIMEOUT, "timeout");
expect(err).toBeInstanceOf(Error);
});

it("has the correct name", () => {
const err = new SdkError(SdkErrorCode.RATE_LIMITED, "rate limited");
expect(err.name).toBe("SdkError");
});

it("captures a stack trace", () => {
const err = new SdkError(SdkErrorCode.INSUFFICIENT_FUNDS, "insufficient funds");
expect(err.stack).toBeDefined();
});
});

describe("SdkErrorCode enum", () => {
it("has all required values", () => {
expect(SdkErrorCode.INVOICE_NOT_FOUND).toBe("INVOICE_NOT_FOUND");
expect(SdkErrorCode.INSUFFICIENT_FUNDS).toBe("INSUFFICIENT_FUNDS");
expect(SdkErrorCode.DEADLINE_EXPIRED).toBe("DEADLINE_EXPIRED");
expect(SdkErrorCode.INVALID_RECIPIENT).toBe("INVALID_RECIPIENT");
expect(SdkErrorCode.CONTRACT_REJECTED).toBe("CONTRACT_REJECTED");
expect(SdkErrorCode.NETWORK_TIMEOUT).toBe("NETWORK_TIMEOUT");
expect(SdkErrorCode.RATE_LIMITED).toBe("RATE_LIMITED");
});
});

describe("isSdkError", () => {
it("returns true for SdkError instances", () => {
const err = new SdkError(SdkErrorCode.CONTRACT_REJECTED, "rejected");
expect(isSdkError(err)).toBe(true);
});

it("returns false for regular Error instances", () => {
const err = new Error("regular error");
expect(isSdkError(err)).toBe(false);
});

it("returns false for null", () => {
expect(isSdkError(null)).toBe(false);
});

it("returns false for plain objects", () => {
expect(isSdkError({ code: "INVOICE_NOT_FOUND" })).toBe(false);
});

it("returns false for strings", () => {
expect(isSdkError("error")).toBe(false);
});
});