Easy Labs
SDKsNode.js

Error handling

Error classes, retry semantics, and idempotency.

Every non-2xx response is thrown as a single class:

import { EasyApiError } from "@easylabs/node";

class EasyApiError extends Error {
  readonly status: number;
  readonly code: string | null;
  readonly details: unknown;
  readonly retryAfterSeconds: number | null;
  readonly raw: unknown;
}
  • status — HTTP status from the response.
  • code — short machine-readable code from the API error body (e.g. RATE_LIMITED, VALIDATION_ERROR), or null when the body wasn't a structured error.
  • details — arbitrary structured payload from the API (e.g. field-level validation errors).
  • retryAfterSeconds — parsed from the Retry-After response header, or null.
  • raw — the original parsed JSON body for forensic logging.

message always begins with API request failed: … for backwards compatibility with code that previously matched against a plain Error.

Retryable vs. non-retryable

StatusClassRetry?Notes
400 / 422EasyApiErrorNoValidation. Surface err.details to the user.
401 / 403EasyApiErrorNoBad / revoked key. Stop retrying and rotate.
404EasyApiErrorNoNot found.
409EasyApiErrorNoConflict — usually a duplicate idempotency key with a different body.
429EasyApiErrorYesHonour err.retryAfterSeconds before retrying.
5xxEasyApiErrorYesTransient — back off and retry.
NetworkTypeError (from fetch)YesTimeouts, DNS, TLS — wrap with your own retry.
async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const retriable =
        (err instanceof EasyApiError && (err.status === 429 || err.status >= 500)) ||
        err instanceof TypeError;
      if (!retriable || attempt >= max) throw err;
      const delay =
        err instanceof EasyApiError && err.retryAfterSeconds
          ? err.retryAfterSeconds * 1000
          : 250 * 2 ** (attempt - 1);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

Idempotency keys

A few create endpoints accept an idempotency_key (or similar) inside the request body — most notably reportSubscriptionUsage and payInvoice. Pass a stable UUID per logical operation so retries safely deduplicate on the server:

import { randomUUID } from "node:crypto";

await easy.payInvoice("inv_123", {
  instrument_id: "PI_...",
  idempotency_key: randomUUID(),
});

The 0.1 SDK does not yet send a top-level idempotency header on every POST; if you need at-least-once safety on endpoints that don't accept a body-level key, deduplicate at the application layer.

Network errors

The SDK is built on the platform fetch. Connection failures (timeouts, DNS, TLS) propagate as Node's TypeError("fetch failed") with a populated cause. They are not wrapped in EasyApiError because no HTTP exchange completed — branch on err instanceof EasyApiError first, and treat anything else as a transport-level failure to retry or surface separately.

On this page