Docs / Errors

Available in v0.1

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
404domain_not_foundValidationErrorno
422from_domain_mismatch, domain_not_bound, worker_not_deployedValidationErrorno
429rate_limitedRateLimitErroryes
502send_failed, decode_failed, internal_errorUpstreamErroryes
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), 429 (rate-limited), and network-level failures (DNS, connection reset, fetch abort). 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.
  • 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 not idempotent — the SDK backend writes a new row on every call. If your caller can retry after a transient failure, build a request id into the subject line or your own dedupe key before forwarding to fm.send.

Next up