Skip to content

Errors

Every failure throws a subclass of CrateError. You branch on a stable kind discriminant and a machine-branchable code — never on the message. Client-side errors also carry a human hint and a copy-pasteable next.

kindclassretryablekey fields
apiCrateAPIErroriff 429/500/503/504status, retryAfter, rateLimit, requestId, details, raw
networkCrateNetworkErroryescause
timeoutCrateTimeoutErroryestimeoutMs
abortCrateAbortErrornocause
validationCrateValidationErrornocode, hint, next, param
not_foundCrateNotFoundErrornohint, next
parseCrateParseErrornostatus, raw

CRATE_ERROR_KINDS and CRATE_ERROR_CODES are exported arrays; CRATE_ERROR_REGISTRY maps each kind to { retryable, clientSide, carries, whenThrown } so an agent can introspect without docs.

StatusBecomesNotes
401validation (api_key_required)usually caught locally before the request
402api (payment_required)key lacks access to the resource
404not_foundalso the SDK’s honest-gap path for unresolved locators
429api (rate_limited)isRateLimited(err)retryAfter + rateLimit
5xxapi (retryable)auto-retried within the deadline
(no response)network / timeouttransport failures
import { Crate, isCrateError, isRateLimited } from '@hosaka-fm/crate';
try {
await crate.artist('Four Tet');
} catch (err) {
if (!isCrateError(err)) throw err; // not ours — rethrow
switch (err.kind) {
case 'validation':
case 'not_found':
console.error(`${err.code}: ${err.hint}${err.next}`);
break;
case 'api': // CrateAPIError
console.error(`HTTP ${err.status} (${err.code}) req=${err.requestId}`);
if (isRateLimited(err)) await wait(err.retryAfter); // SDK already backed off
break;
default:
console.error(JSON.stringify(err));
}
}

switch (err.kind) narrows the union: inside case 'api', err is a CrateAPIError with .status etc. Always log err.requestId (present on every CrateAPIError) when contacting support.

import { isCrateError, isCrateAPIError, isRateLimited, isRetryable } from '@hosaka-fm/crate';

Prefer the isCrate* guards. They both narrow the type and survive the ESM/CJS dual-package boundary, where instanceof can see two distinct copies of a class and silently return false. isRetryable(err) tells you whether the SDK’s policy would retry; isRateLimited(err) narrows to a 429 CrateAPIError.

log(JSON.stringify(err)); // a plain Error → "{}"; a CrateError → full teaching envelope

CrateError.toJSON() emits a stable CrateErrorJSON envelope (name, kind, code, message, status?, retryAfter?, requestId?, rateLimit?, hint?, next?, …). It deliberately omits .raw and reduces .cause to { name, message }, so logs and agent-to-agent handoffs never leak response bodies or headers. The .raw field stays on the live object as an escape hatch for fields the SDK doesn’t model.

A 200 with present: false / cluster_id: null / an empty list is an honest gap, surfaced as null (or an empty array), not an exception. crate.artistOrNull(...) returns null, and crate.resolve(...) returns a null cluster_id, in that case. Only 4xx/5xx throw. Runnable: examples/error-handling.ts.