Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Errors

Every failure throws one RevenueError with a normalized code — how to branch on it, where the provider body lives, and what the client retries for you.

Every failure in revenue-sdk throws a single error type — RevenueError — with a normalized code, so you branch on the failure without parsing provider-specific statuses or message strings.

Branch on code

import { RevenueError } from 'revenue-sdk';

try {
  await client.subscriptions.changePlan({ id, product });
} catch (error) {
  if (!(error instanceof RevenueError)) throw error;

  switch (error.code) {
    case 'unauthorized':
      // credentials are wrong or revoked
      break;
    case 'not_found':
      // the subscription or product no longer exists
      break;
    case 'payment_required':
      // the customer's payment failed — surface it, don't retry
      break;
    case 'unsupported':
      // this provider lacks the capability — check client.capabilities
      break;
    default:
      if (error.retryable) {
        // transient — retry with back-off, waiting error.retryAfter seconds if set
      }
  }
  throw error;
}

RevenueErrorCode is a closed union of ten values: unauthorized, forbidden, not_found, conflict, rate_limited, payment_required, validation, unsupported, provider_error, network_error. Alongside code, every error carries provider, status, retryable, retryAfter, cause, and responseBody. The full field reference, the HTTP-status mapping and the per-provider quirks are in Error codes.

validation and unsupported are frequently raised before any HTTP request — a bad parameter, or a capability the active provider does not have.

cause is the JS error, responseBody is the provider’s

cause is the ordinary Error chain: the TypeError fetch throws on a dead connection, or the SyntaxError from a response that wasn’t JSON. The provider’s own error payload lives on responseBody, installed as a non-enumerable property — console.error(error), util.inspect, JSON.stringify(error) and reporters such as Sentry all skip it, so a provider body never lands in your logs by accident, while error.responseBody reads it whenever you want it.

Known secrets are redacted from error.message before the error is constructed. responseBody is deliberately not redacted — it holds customer PII no secrets list can enumerate, and non-enumerability is what keeps it out of logs. The same caveat applies to the enumerable raw field on every model, which serializes normally and appears on success too.

The client retries once

The client retries at most once per call: on rate_limited anywhere, and on any other retryable error (network_error, 5xx) for reads only. A rate limit is safe to replay because the provider rejected the request before doing any work; a transport failure is not, since the request may have landed and no SDK write carries an idempotency key. The wait is retryAfter, or one second when the provider sent no Retry-After, and it is skipped entirely above maxRetryAfterSeconds:

const client = createClient({
  provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
  retry: { maxRetryAfterSeconds: 10 }, // 0 disables the retry entirely
});

The retry covers every page fetched inside listAll, and its wait is interrupted by the signal you passed. usage.report is the one call it never covers — see Usage-based billing.

Everything beyond that — exponential back-off, queues, dead-lettering — is yours to build on retryable and retryAfter.

Last updated on August 8, 2026

Was this page helpful?