TESTNET
Markets
Trade
Lending Vaults
More
User Docs Developer Docs Sdk API Docs Help
Overview
Overview
Architecture
Authentication
Client configuration
Overview
Environments
Installation
Market data
Market data services
Authentication model
Quickstart
Trading
Trading services
Account services
Catalog & precision
Streaming
Accounts & balances
Funding services
Deposits & withdrawals
Realtime client
Errors
Server-side usage
Error handling
  1. Typescript
  2. /
  3. Error handling

Error handling

Every error the SDK raises extends PolyesterError, which carries a stable machine-readable code and a retryable flag. The hierarchy splits on retryability, so triage is one instanceof:

import { PolyesterError, TransientError, RateLimitError } from "@polyester/sdk";

try {
	await client.orders.create(input);
} catch (err) {
	if (err instanceof RateLimitError) {
		await sleep(err.retryAfterMs ?? 1000);
		// retry with the same clientOrderId
	} else if (err instanceof TransientError) {
		// network / timeout / unavailable — safe to retry
	} else if (err instanceof PolyesterError) {
		console.error(err.code, err.message); // permanent — fix the input or auth
	} else {
		throw err;
	}
}

The full tree, with every class and code, is in the Errors reference. The shape in brief:

  • TransientError (retryable) → NetworkError, TimeoutError, RateLimitError, ServiceUnavailableError
  • RequestError (permanent) → ValidationError, ResourceNotFoundError, AlreadyExistsError, PermissionError, AuthenticationError, PreconditionFailedError, ConfigurationError, MfaRequiredError (and its three subclasses)
  • InternalServerError — backend failure; not automatically retryable

RPC failures keep the original ConnectRPC error as err.cause.

Retrying safely

Transient failures may have happened after the backend applied a mutation. Retry with the same idempotency key so the backend dedupes:

import { isRetryableError } from "@polyester/sdk";

async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
	for (let i = 0; ; i++) {
		try {
			return await fn();
		} catch (err) {
			if (i >= attempts - 1 || !isRetryableError(err)) throw err;
			await sleep(500 * 2 ** i);
		}
	}
}

const clientOrderId = crypto.randomUUID();
await withRetry(() => client.orders.create({ ...input, clientOrderId }));

orders.modify and orders.cancelAll accept a requestId for the same purpose.

Cancellation

Every request method accepts an AbortSignal:

const controller = new AbortController();
const promise = client.marketOverview.list({}, { signal: controller.signal });
controller.abort();

Aborts are deliberately not PolyesterErrors — detect them with isAbortError(err) and treat them as flow control, not failures.

MFA: step-up and elevation

Sensitive mutations can require a second factor. The SDK signals this with dedicated error types:

Error Meaning Your move
MfaEnrollmentRequiredError User has no MFA factor Send them through enrollment
StepUpRequiredError Needs a fresh one-use proof Run a challenge, retry with stepUpToken
SessionElevationRequiredError Needs a recently elevated session Run an elevation challenge, retry

The step-up flow:

import { StepUpRequiredError } from "@polyester/sdk";

try {
	await client.apiKeys.create(payload);
} catch (err) {
	if (err instanceof StepUpRequiredError) {
		const challenge = await client.mfa.beginChallenge({ purpose: "freshStepUp" });
		const { stepUpToken } = await client.mfa.verifyTotpChallenge({
			challengeId: challenge.challengeId,
			code: userEnteredCode,
		});
		await client.apiKeys.create(payload, { stepUpToken });
	} else {
		throw err;
	}
}

Catch MfaRequiredError to handle all three cases with one branch. Type-guard helpers (isFreshStepUpRequiredError, isSessionElevationRequiredError, isMfaEnrollmentRequiredError) are exported for code that works with unknown errors.

Validation and precision errors

Client-side validation throws before any network I/O:

  • ValidationError — the input shape is wrong (unknown keys, missing fields, bad enum values).
  • CatalogConversionError — a decimal string has excess precision or is not a valid decimal ("0.1234567" on a 6-decimal field). The SDK never rounds for you.
  • CatalogValidationFailedError — order input violates pair constraints (tick size, step size, min quantity, min notional) when using catalog.orders.assertSpotOrderDecimalInput.
  • CatalogLookupError / CatalogNotReadyError — unknown symbol/asset, or a direct catalog read before ensureReady().
Validate before submit
For form UX, run client.catalog.orders.validateSpotOrderDecimalInput(...) to get a structured list of violations instead of catching a thrown error.

Streaming errors

Subscriptions never throw — failures flow to your onError handler as an SdkSubscriptionErrorContext (channel, type, error). Reconnects are automatic; snapshot-then-stream subscriptions also refetch their snapshot after a reconnect. See Streaming.

Previous

Server-side usage

  • Retrying safely
  • Cancellation
  • MFA: step-up and elevation
  • Validation and precision errors
  • Streaming errors