Error Handling
Catch listingsapi-js error classes, read platform error codes, and build retryAfter-aware backoff.
Every error the SDK throws derives from ListingsAPIError. API failures
throw APIError or one of its subclasses, whether the platform reports
them as a non-2xx HTTP status or inside an HTTP 200 body. Network
failures (DNS errors, refused connections, timeouts) throw
APIConnectionError. You never inspect success flags or raw payloads:
a resolved promise means the call succeeded.
Error hierarchy
ListingsAPIError base class for everything the SDK throws
├── APIError any failure reported by the API
│ ├── AuthenticationError 401, or invalid-token error payloads
│ ├── PermissionDeniedError 403, or permission error payloads
│ ├── NotFoundError 404, resource not found
│ ├── ValidationError 400/422, mutations reporting success=false
│ │ or errors[], and client-side payload checks
│ ├── RateLimitError 429, carries retryAfter
│ └── InternalServerError 5xx, safe to retry
└── APIConnectionError network failure, nothing reached the API
All classes are exported from the package root:
import {ListingsAPIError,APIError,AuthenticationError,PermissionDeniedError,NotFoundError,ValidationError,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js';APIError properties
| Property | Type | Description |
|---|---|---|
statusCode | number | null | HTTP status of the failed response: 401, 429, 500, or 200 when the error arrived inside a 200 body. null for client-side validation failures. |
code | string | null | First platform error code, e.g. SY10005, when the API sent one. |
errors | ApiErrorEntry[] | Every error entry, normalized to { code, message, context }. |
responseBody | string | null | Raw response body as a string. |
retryAfter | number | null | RateLimitError only. Seconds to wait before retrying, parsed from the Retry-After header, or null when the header was absent. |
message | string | Human-readable summary with the error entries (or raw body) appended. |
name | string | The concrete class name, e.g. "RateLimitError". |
import { ListingsAPI, APIError, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIError) { console.error(error.statusCode); // 401, 429, 500, or 200 for payload errors console.error(error.code); // first platform code, e.g. 'SY10005' console.error(error.errors); // [{ code, message, context }, ...] console.error(error.responseBody); // raw body text} else if (error instanceof APIConnectionError) { console.error('Nothing reached the API:', error.message);}}Catching errors by type
Authentication failures
import { AuthenticationError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 1 });} catch (error) {if (error instanceof AuthenticationError) { console.error('Invalid API key. Rotate it under API Keys in the dashboard.');}}Missing resources
import { NotFoundError } from 'listingsapi-js'; try {await client.fetchPremiumListings(99999999);} catch (error) {if (error instanceof NotFoundError) { console.error('Location not found.');}}Rate limits
RateLimitError.retryAfter holds the server's Retry-After value in
seconds, or null when the header was absent:
import { RateLimitError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 100 });} catch (error) {if (error instanceof RateLimitError) { const waitSeconds = error.retryAfter ?? 5; console.error('Rate limited, retry in ' + waitSeconds + 's');}}Validation failures
Server-side validation failures carry the platform's error entries:
import { ValidationError } from 'listingsapi-js'; try {await client.updateLocation({ id: 16808, phone: 'not-a-phone-number' });} catch (error) {if (error instanceof ValidationError) { for (const entry of error.errors) { console.error((entry.code ?? 'unknown') + ': ' + entry.message); }}}Errors inside HTTP 200 responses
The platform reports some failures with HTTP 200 and an error payload in the body. The SDK inspects every response and throws for these too, so a 200 status never means "check the body yourself":
| Payload signal | Thrown as |
|---|---|
Top-level errors[] with code SY90005 or SY90001 | AuthenticationError |
Top-level errors[] with code SY90003 | PermissionDeniedError |
Top-level errors[] with code RATE_LIMITED | RateLimitError |
Any other top-level errors[] | APIError |
Mutation result with errors[] or success: false | ValidationError |
For mutations (POST and DELETE helpers), the SDK checks every result
object in the response envelope; if any reports success: false or
carries errors[], it throws ValidationError with statusCode set to
200.
import { ValidationError } from 'listingsapi-js'; try {const result = await client.archiveLocations([16808]);console.log('Archived:', result);} catch (error) {if (error instanceof ValidationError) { // The API answered 200 but rejected the mutation console.error(error.statusCode); // 200 console.error(error.message);}}Client-side validation in the post helpers
The post helpers (createAnnouncement, createEvent, createOffer, and
bulkPublish) validate their payload locally and throw
ValidationError before any network call:
namemust be non-blank andlocationIdsmust not be empty.sitesmust be a non-empty subset ofGOOGLEandFACEBOOK.messageis required; when it is a per-site map, every targeted site needs a non-blank entry.ctaTypeandctaUrlmust be provided together, andctaTypemust be one ofBOOK,ORDER,SHOP,LEARN_MORE,SIGN_UP,GET_OFFER.createEventandcreateOfferadditionally requiretitle.postType(onbulkPublish) must be one ofANNOUNCEMENT,EVENT,OFFER,COVID19,PRODUCT.
These local errors have statusCode: null, which is how you tell them
apart from a server-side rejection. The low-level createPost(body)
method sends its raw body without these checks.
import { ListingsAPI, ValidationError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.createAnnouncement({ name: 'Grand opening', locationIds: [16808], message: 'Acme Dental is now open in Midtown!', ctaType: 'BOOK', // ctaUrl missing: throws before any request is sent});} catch (error) {if (error instanceof ValidationError && error.statusCode === null) { console.error(error.message); // Invalid post: ctaType and ctaUrl must be provided together}}Built-in retries
The client itself retries 429 and 5xx responses (honoring the
Retry-After header) and network failures, twice by default. Errors
reach your code only after those attempts are exhausted. See
Configuration for the maxRetries and
timeout options.
A retryAfter-aware backoff recipe
For long-running jobs that should outlast the client's built-in retries,
wrap calls in a backoff loop that prefers the server's retryAfter hint
and falls back to exponential delays:
import {ListingsAPI,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js'; const client = new ListingsAPI(); async function withBackoff<T>(fn: () => Promise<T>,maxAttempts = 5,): Promise<T> {for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { const retryable = err instanceof RateLimitError || err instanceof InternalServerError || err instanceof APIConnectionError; if (!retryable || attempt === maxAttempts) throw err; let delayMs = 500 * 2 ** (attempt - 1); if (err instanceof RateLimitError && err.retryAfter !== null) { delayMs = err.retryAfter * 1000; } await new Promise((resolve) => setTimeout(resolve, delayMs)); }}throw new Error('unreachable');} const page = await withBackoff(() => client.fetchAllLocations({ first: 25 }));NotFoundError, ValidationError, AuthenticationError, and
PermissionDeniedError are deliberately not retried: repeating those
requests cannot succeed without changing the input or the key.
Network failures and timeouts
Anything that prevents a response from arriving (DNS failure, refused
connection, or the per-request timeout firing) throws
APIConnectionError. The SDK never leaks the native TypeError or
AbortError from fetch.
Timeouts are enforced internally with AbortSignal.timeout on every
attempt, 240000 ms by default, configurable via the timeout
constructor option. A timed-out attempt is retried like any other
connection failure; only the final failure surfaces:
import { ListingsAPI, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI({ timeout: 15000 }); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIConnectionError) { console.error('Network failure or timeout:', error.message);}}Related resources
- Configuration, the
timeoutandmaxRetriesoptions - Quickstart, minimal error handling example
- Use cases, real-world error handling in complete scripts