Docs / Errors

Available in v1.0

Errors & retries

Every SDK failure surfaces as one of five typed classes. The code string is the public contract — never branch on the human-readable message.

Error envelope

Every non-2xx response carries a JSON body with a stable shape. The error field is the machine-readable code;detail and other extra fields are diagnostic — your code should branch on code, not detail.

422 — samplehttp
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": "from_domain_mismatch",
  "fromDomain": "attacker.example.com",
  "boundDomain": "yourdomain.com"
}

HTTP → code matrix

The same code may appear under more than one HTTP status (e.g. missing_or_invalid_bearer can be 400 or 401). Always match the class first, then the code if you need finer discrimination.

HTTPCodesSDK classRetryable?
400invalid_json, validation_failed, missing_or_invalid_bearerValidationErrorno
401api_key_invalid, api_key_revokedAuthenticationErrorno
403scope_deniedValidationErrorno
404domain_not_found, message_not_foundValidationErrorno
409idempotency_key_conflict, sending_not_configured, domain_scope_missingValidationErrorno
422from_domain_mismatch, from_domain_not_bound, domain_not_bound, worker_not_deployedValidationErrorno
429rate_limit_exceeded, idempotency_key_in_progressRateLimitErrorsee below
502send_failed, decode_failed, internal_errorUpstreamErroryes
503idempotency_unavailableUpstreamErroryes
networknetwork_errorUpstreamErroryes

The FlowmailsError hierarchy

Every error the SDK throws is an instance of FlowmailsError. Subclasses give you a faster way to discriminate without parsing the HTTP status or code string. Use instanceof — the SDK guarantees the prototype chain.

ClassHTTPRetryableNotes
AuthenticationError401noCarries code = api_key_invalid or api_key_revoked.
ValidationError400 / 422noAdds detail with the human-readable reason. Surface this to logs.
RateLimitError429yesBackoff and retry. The 429 you see is the platform’s safety floor; no per-key Retry-After header yet.
UpstreamError502 / 504 / 408yesAdds upstreamMessage when the SDK backend forwards a receive-worker detail.
FlowmailsErrorothernoCatch-all for unexpected HTTP statuses; surfaces code + raw message.

Discriminating with instanceof

The SDK auto-retries transient failures up to maxRetries (default 2) before throwing. Once a non-retryable class reaches your code, the request has already been attempted the right number of times — re-throwing a retry loop on top is the wrong move and burns CPU.

error-handling.tsts
import {
  Flowmails,
  RateLimitError,
  UpstreamError,
  ValidationError,
  AuthenticationError,
  FlowmailsError,
} from "@flowmails/flowmails-sdk";

try {
  const { id } = await fm.send({
    from: "support@yourdomain.com",
    to: "customer@example.com",
    subject: "Order #1234",
    text: "Confirmed.",
  });
  console.log("queued", id);
} catch (err) {
  if (err instanceof RateLimitError || err instanceof UpstreamError) {
    // The SDK has already retried maxRetries times — give up and
    // route to a queue / dead-letter for human attention.
    await enqueueRetry(err);
  } else if (err instanceof AuthenticationError) {
    // Surface to ops: the key was rotated or revoked.
    await notifyOps("sdk-key-revoked", err);
  } else if (err instanceof ValidationError) {
    // The payload is wrong. Log err.code + err.detail; do NOT retry.
    logger.warn({ code: err.code, detail: err.detail }, "sdk validation");
  } else if (err instanceof FlowmailsError) {
    // Unknown SDK failure — surface the code.
    logger.error({ code: err.code, status: err.status }, "sdk failure");
  } else {
    // Non-SDK exception (your code, the runtime, etc).
    throw err;
  }
}

Retry policy in detail

  • Retried. 5xx, 408 (request timeout), and network-level failures (DNS, connection reset, fetch abort). Plus exactly one 429: idempotency_key_in_progress — a retry there replays the first answer rather than sending twice, so retrying it is the whole point. Exponential backoff with jitter, capped at 4 s. Default maxRetries is 2 — set to 0 to disable.
  • Not retried. 4xx is deterministic — retrying a 400, 401, 404, or 422 just burns CPU. Fix the request before sending again. AuthenticationError and ValidationError fall in this bucket even if the underlying status happens to be a 5xx one day.
  • Not retried: the send quota. rate_limit_exceeded is a 429 that a retry cannot fix — the window is an hour, and a retry would burn another slot. It carries limit, remaining and resetAt (ISO-8601), plus the X-Flowmails-RateLimit-Remaining / X-Flowmails-RateLimit-Reset headers and Retry-After, so you can schedule the next attempt instead of guessing.
  • Timeout. Each request is bounded by a 15-second AbortSignal.timeout. The cap matches the SDK backend’s upstream timeout, so you get a clean network error rather than the backend’s generic 502.
  • Idempotency. send is idempotent when you give it a key: pass idempotencyKey and a retry replays the first answer instead of sending twice. The key is scoped to your account, so two accounts may use the same string. The same key with a different payload is a bug on your side and answers idempotency_key_conflict (409) rather than quietly returning the first response. Without a key, every call sends — which is the right default for a caller that does its own dedupe.

Next up