Errors and retries

View as markdown

How failed requests are reported, and which requests are retried.

Errors

Failed requests reject with a SynthientError or one of its subclasses. Check them with instanceof:

import { APIError } from "@synthient/synthient";

try {
  await client.lookupIP("8.8.8.8");
} catch (error) {
  if (!(error instanceof APIError)) {
    throw error;
  }
  switch (error.status) {
    case 402:
      console.log("out of lookup credits");
      break;
    case 429:
      if (error.retryAfter === undefined) {
        console.log("monthly request budget spent");
      } else {
        console.log(`rate limited, retry after ${error.retryAfter} ms`);
      }
      break;
    default:
      console.log("request", error.requestID, "failed:", error.detail || error.title);
  }
}
ClassExtendsThrown when
SynthientErrorErrorA request failed or a response was invalid, such as a body that is not a JSON object. It is the base class of the others.
APIErrorSynthientErrorThe API, or the storage host serving an export download, answered with a non-success HTTP status. A storage error carries only status and retryAfter.
APIConnectionErrorSynthientErrorThe connection failed before a response arrived or while its body was being read, for example because of a DNS, connection, or TLS failure, or a stream that dropped. The underlying error is its cause, except when the storage host that serves an export download fails, where it is omitted to keep presigned URLs out of errors.
APITimeoutErrorAPIConnectionErrorNo data arrived for the client's timeout while waiting for a response or reading its body. It has no cause.
  • Names: Each class sets name to its own name, such as "APIError".
  • Before a request: Invalid arguments and configuration fail with a TypeError or RangeError instead, before any request is sent.
  • Cancellation: A canceled call rejects with its AbortSignal's reason.

See Errors for what each status means.

APIError

  • Name
    status
    Type
    number
    Description

    The HTTP status code, such as 402 when the account is out of credits.

  • Name
    type
    Type
    string
    Description

    The problem type URI, such as "https://docs.synthient.com/errors/unauthorized", or "".

  • Name
    title
    Type
    string
    Description

    A short summary of the problem type, or "".

  • Name
    detail
    Type
    string
    Description

    An explanation of this occurrence of the problem, or "". For a plain-text error, such as the edge's 401 for a malformed key, it is the response body.

  • Name
    errors
    Type
    Record<string, string[]>
    Description

    Validation messages by request field, set on 422 responses. It is an empty object when the response has none.

  • Name
    requestID
    Type
    string
    Description

    Identifies the request when contacting Synthient support, or "". It comes from the X-Request-ID header, or the problem's instance when the header is absent.

  • Name
    retryAfter
    Type
    number | undefined
    Description

    Milliseconds the server asked the caller to wait before retrying, or undefined if it did not say.

The fields are read-only.

  • Logging: message contains only the HTTP status code and its standard description, such as 402 Payment Required, so it is safe to log.
  • Untrusted fields: The other fields come from the server's problem details and can contain sensitive data, so inspect them explicitly.

APIError can be constructed, for example to simulate failures in tests.

constructor(init: APIErrorInit, options?: ErrorOptions);
  • Input: Only init.status is required. options takes the standard Error options, such as cause.

APIErrorInit

  • Name
    status
    Type
    number
    Description

    Sets APIError.status.

  • Name
    type?
    Type
    string | undefined
    Description

    Sets APIError.type. Defaults to "".

  • Name
    title?
    Type
    string | undefined
    Description

    Sets APIError.title. Defaults to "".

  • Name
    detail?
    Type
    string | undefined
    Description

    Sets APIError.detail. Defaults to "".

  • Name
    errors?
    Type
    Record<string, string[]> | undefined
    Description

    Sets APIError.errors. Defaults to an empty object.

  • Name
    requestID?
    Type
    string | undefined
    Description

    Sets APIError.requestID. Defaults to "".

  • Name
    retryAfter?
    Type
    number | undefined
    Description

    Sets APIError.retryAfter, in milliseconds.

Retries and timeouts

Which requests are retried:

  • Account and feed requests (account, the export methods, and stream handshakes) are retried on 500, 502, 503, and 504 responses, on 429 responses that carry Retry-After, on idle timeouts, and on connection failures with one of these codes: ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT, ENOTFOUND, EAI_AGAIN, ENETUNREACH, EHOSTUNREACH, UND_ERR_SOCKET, or UND_ERR_CONNECT_TIMEOUT.
  • An error thrown by a custom fetch is retried the same way when it, or an error in its cause chain, has one of these codes as its code.
  • A failure while reading the JSON body of an account, listExports, or exportMeta response is retried the same way, within the same retry budget.
  • Other failures, such as a TLS certificate error, are not retried.
  • Billed lookups (lookupIP, lookupIPs, and lookupDomain) and health are retried only on 429 responses that carry Retry-After, which the rate limiter returns before doing any work, so a retried lookup is never charged twice.
  • A 429 without Retry-After means the key's monthly request budget is spent or it has too many open streams, so it rejects without a retry.
  • Once a stream is open, and for downloads from the export storage host, nothing is retried.

How retries wait:

  • Retries back off exponentially from one second, doubling up to 30 seconds, with 25% jitter.
  • The wait is never shorter than Retry-After.
  • A Retry-After longer than a minute rejects with the response's APIError instead of retrying.
  • Aborting the call's signal stops a wait.
  • Set the retry count with maxRetries.

The client's timeout is an idle timeout, 60 seconds by default.

  • It bounds each wait for response headers and for the next part of a body.
  • It runs only while the client waits on the network, so a slow consumer does not trip it.
  • An idle timeout fails the attempt with an APITimeoutError.

The timeout does not limit a whole call. Bound a call, including its retries, with AbortSignal.timeout:

const info = await client.lookupIP("8.8.8.8", { signal: AbortSignal.timeout(10_000) });
console.log(info.ip);

When the signal fires, the call rejects with its reason, which for AbortSignal.timeout is a DOMException named "TimeoutError".