Cubicle

Errors and retries

Error classes in @cubicle/sdk (CubicleError, CubicleApiError, CubicleConnectionError, CubicleTimeoutError, CubicleBoxError, CubicleOperationError) with their codes and fields, branching with instanceof, automatic retries with Retry-After and backoff, what is safe to repeat, and idempotency keys.

Error classes

Every error the SDK raises is a CubicleError with a stable, machine-readable code and a readable message. Branch on the class and the code, not on the message.

Classcode valuesWhenUseful fields
CubicleApiErrorThe API's code, such as not_found, conflict or rate_limited; http_<status> when the response had noneThe API answered with an error status, after any retries.status, detail, retryAfterSeconds
CubicleConnectionErrorconnection_failed, request_timeout, invalid_responseThe API could not be reached, did not answer within requestTimeoutMs, or sent a success response that is not JSON.cause
CubicleTimeoutErrorwait_timeoutA wait ran out of time.last
CubicleBoxErrorbox_failed, restart_required, hibernation_failed, box_supersededA Box cannot reach the state being waited for.box
CubicleOperationErroroperation_failed, operation_unknownAn exec, snapshot or restore operation ended failed or unknown.operation
CubicleErrormissing_api_url, missing_api_key, invalid_option, org_required, missing_resultA configuration problem, or a finished operation whose result the SDK could not find.code
  • An aborted signal is not a CubicleError. The call rejects with the signal's reason; see Stop waiting with a signal.
  • last is the most recent BoxInfo or Operation a timed-out wait read. box is the Box's latest record, and box.observed?.error says why a Box failed. operation.error says why an operation failed.
  • operation_unknown means an exec was interrupted: its Node's service restarted, or the Box was stopped, hibernated or deleted, or its host restarted, while the command ran. An exec is never replayed, so the command may or may not have run.
  • The CubicleBoxError codes are explained in Wait for a state.
  • org_required is explained in The default organization, and invalid_option in Client options.
  • A CubicleApiError has the API's status, code and message. Every code, with its cause and what to do, is in Errors, and each method's reference, such as Boxes, lists the codes it rejects with. The SDK may retry rate_limited and capacity first; see Retries.

Branch on errors

import {
  Cubicle,
  CubicleApiError,
  CubicleBoxError,
  CubicleConnectionError,
  CubicleOperationError,
  CubicleTimeoutError,
} from "@cubicle/sdk"

const cubicle = new Cubicle()

try {
  const result = await cubicle.boxes.exec("acme/job-1", ["make", "test"], { wake: true })
  console.log(result.exitCode) // a non-zero exit code is a result, not an error
} catch (error) {
  if (error instanceof CubicleOperationError) {
    // operation_failed: operation.error says why. operation_unknown: it may or may not have run.
    console.error(error.code, error.operation.error)
  } else if (error instanceof CubicleTimeoutError) {
    console.error("timed out; last record read:", error.last)
  } else if (error instanceof CubicleBoxError) {
    console.error("the Box did not become ready:", error.code, error.box.observed?.error)
  } else if (error instanceof CubicleApiError && error.code === "not_found") {
    console.error("no Box acme/job-1")
  } else if (error instanceof CubicleApiError) {
    console.error(error.status, error.code, error.message)
  } else if (error instanceof CubicleConnectionError) {
    console.error("cannot reach the Cubicle API:", error.code)
  } else {
    throw error // for example, an aborted signal's reason
  }
}

Retries

The SDK retries each request up to maxRetries times, 3 by default, after these failures:

FailureDelay before the retry
429 rate_limitedThe Retry-After value
502, 503 or 504Retry-After if sent, otherwise a backoff
A connection failure, or no answer within requestTimeoutMsA backoff
  • Any other status, including 403 blocked, reaches you at once.
  • A Retry-After longer than 60 s is not slept. The error reaches you at once, with retryAfterSeconds set.
  • The backoff is exponential with jitter: 0.25 to 0.5 s before the first retry, doubling each time up to 4 to 8 s. With the default 3 retries and no Retry-After, a failing request spends 1.75 to 3.5 s in backoff before its error reaches you.
  • 503 capacity is retried like any 503, so it reaches you after those retries.
  • maxRetries: 0 turns retries off. Aborting the signal also ends a retry delay.
  • A wait's polls do not use maxRetries: they keep polling until the wait's timeout. See Waiting.

A request that failed without an answer may still have reached Cubicle. The SDK repeats a request only when that is safe, and it makes every request it sends safe to repeat:

  • Reads and delete are safe to repeat. delete looks the Box up first and deletes it by ID, so a retry cannot reach a newer Box that took the name, and a retry that finds the Box already deleted counts as success.
  • create, exec, snapshot and restore carry an idempotency key.
  • start, stop and hibernate take no key, but they set a desired state, so repeating one leaves the same state. A repeated hibernate has no further effect. A start or stop repeated after a lost response advances the Box's generation once more, which pauses traffic to a running Box without restarting it.

Repeated rate-limit refusals block your IP address; see Rate limits.

Idempotency keys

create, exec, snapshot and restore send an Idempotency-Key header. The SDK generates a key for each call and sends the same key on every retry of that call. A retry after a lost response therefore returns the original Box or operation: it never creates a second Box or runs a command twice.

Pass your own idempotencyKey to make a whole call repeatable, for example when your job restarts and runs the same step again. A repeated call with the same key and parameters returns the original Box or operation, and then waits for it as usual:

// Safe to run again after a crash: a second run returns the first run's result.
const result = await box.exec(["sh", "-c", "make migrate"], {
  idempotencyKey: "migrate-2026-11-05",
})

The rules for keys, such as their length, how long they are kept and what a repeat returns, are in Idempotency keys. Recover a failed create shows a complete example.

On this page