Back to Arky

Error Handling

Handle SDK and durable-request failures safely

SDK methods resolve directly to their declared response type. Failed requests throw; there is no Result.ok or Result.val wrapper.

The public examples use a storefront client:

import { initialize } from "arky-sdk/storefront";

const arky = initialize(import.meta.env.PUBLIC_ARKY_PUBLISHABLE_KEY, {
	locale: "en",
	market: "us",
});

Request Errors

try {
	const product = await arky.eshop.product.get({
		id: "prod_xyz",
	});

	console.log(product.key);
} catch (error) {
	if (error instanceof Error && error.name === "ApiError") {
		const apiError = error as Error & {
			statusCode: number;
			validationErrors?: Record<string, string[]>;
			requestId?: string;
		};

		console.error(apiError.statusCode, apiError.message, apiError.requestId);
	} else {
		throw error;
	}
}

HTTP failures use name: 'ApiError' and expose statusCode, message, optional validation errors, and an optional request ID. Network failures, aborted requests, and invalid response bodies throw NetworkError, AbortError, and ParseError respectively. Server-side 5xx details are sanitized; use the request ID to correlate a failure with server logs.

Warning

Do not display raw provider or internal error details to customers, and do not log credentials, authorization headers, signing secrets, access tokens, or complete sensitive payloads.

Durable External Effects

Charges, refunds, label purchases, subscription mutations, emails, outgoing webhooks, uploads, and similar provider calls are fail-closed, domain-authoritative effects. When an endpoint requires a caller-owned resource ID such as refund_id, shipment_id, message_id, send_id, delivery_id, or action_id:

  • Persist the immutable request and resource ID before the first request.
  • Reuse it only with the exact same immutable request parameters.
  • Treat unknown as terminal for automatic processing; the provider may or may not have completed the effect.
  • Never generate a fresh UUID in a generic retry loop. A new resource is a new business decision and may duplicate the external effect.

The resource ID, immutable request, typed lifecycle, and provider’s documented contract are Arky’s durable evidence and continuation authority. Clients never supply provider idempotency keys or request fingerprints. Where a provider supports native idempotency, Arky derives a deterministic adapter-level key from the owning resource ID and stage; retryable lifecycles also include their persisted revision. Arky continues an identical mutation only when that provider explicitly documents it as safe, such as Stripe’s same-key network continuation. Otherwise ambiguity becomes terminal unknown; exact provider reads or authenticated webhooks may settle the existing operation but never authorize a replacement mutation.

Authenticated Admin browser clients should use the SDK’s durable-request utility instead of keeping a resource ID in memory. Create that client without a Personal API Token, then complete the normal operator login flow:

import { createAdmin } from "arky-sdk/admin";

const sdk = createAdmin({
	baseUrl: "https://api.arky.io",
	storeId: "store_abc123",
	market: "us",
});

Never initialize an Admin browser client with a Personal API Token. The utility below fails closed when localStorage or Web Locks are unavailable, verifies the exact stored value after writes and clears, and never falls back to volatile storage:

import {
	clearDurableRequest,
	getOrCreateDurableRequest,
	withDurableRequestLock,
} from "arky-sdk/utils";

const request = {
	order_id: "ord_xyz789",
	amount: 1999,
};
const storageKey = `arky:order-refund:store_abc123:${request.order_id}`;

const refund = await withDurableRequestLock(storageKey, "refund", async () => {
	const durable = getOrCreateDurableRequest(storageKey, request, "refund");
	const result = await sdk.eshop.order.createRefund({
		...request,
		refund_id: durable.id,
	});

	if (result.refund_id !== durable.id) {
		throw new Error("Refund response did not match the requested refund");
	}
	clearDurableRequest(durable, "refund");
	return result;
});

If the request is interrupted, the same immutable request reuses the persisted resource ID. A different request is rejected until the saved request has been reviewed. Clear the local record only after Arky durably accepts the resource, verifying the resource identity echoed in the response before clearing it.

Read-only Retries

Retries are suitable only for operations proven to be read-only, such as list, get, and status requests. Use bounded attempts, exponential delay, and cancellation:

async function getWithRetry<T>(
	request: () => Promise<T>,
	attempts = 3,
): Promise<T> {
	let lastError: unknown;

	for (let attempt = 0; attempt < attempts; attempt += 1) {
		try {
			return await request();
		} catch (error) {
			lastError = error;
			const statusCode = (error as { statusCode?: number }).statusCode;
			if (statusCode && statusCode < 500) throw error;
			if (attempt + 1 < attempts) {
				await new Promise((resolve) => setTimeout(resolve, 250 * 2 ** attempt));
			}
		}
	}

	throw lastError;
}

const product = await getWithRetry(() =>
	arky.eshop.product.get({ id: "prod_xyz" }),
);
Warning

Never wrap an external provider mutation in this helper. A timeout or network error does not prove that the provider failed to apply the request.

Application Handling

Map errors at the application boundary. Keep the detailed error for safe diagnostics and show a stable customer-facing message:

async function loadProduct(id: string) {
	try {
		return await arky.eshop.product.get({ id });
	} catch (error) {
		const statusCode = (error as { statusCode?: number }).statusCode;
		if (statusCode === 404) return null;
		throw error;
	}
}