Login
Free Sign Up
Docs
/

The Result-Based Error Model

This explains why the SDK never throws, what a Result is, and how to read a LiganticError.

Why the SDK Never Throws

A fallible SDK operation returns a value that is the outcome, rather than signaling failure by throwing.

The reasons:

  • Failures are data. A network error or a 404 is an expected, handleable outcome — not an exceptional one. Returning it lets you branch on it cleanly without try/catch.
  • One shape everywhere. Every operation returns the same Promise<LiganticResult<T>>, so the handling code is uniform across the whole SDK.
  • No accidental swallowing. Because the SDK doesn't throw, there is no try/catch scope where a failure can be silently ignored. You must inspect .ok.
  • The CLI renders it. The CLI's clean error: ... output is the Err side of the same result, rendered for a human.

The Result Type

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
type LiganticResult<T> = Result<T, LiganticError>;

Every operation returns Promise<LiganticResult<T>>. You check the ok flag:

const schema = await client.schemas.get(spaceId, schemaId);

if (schema.ok) {
  // schema.value is typed as Schema
} else {
  // schema.error is a LiganticError
}

Helpers are exported for convenience:

  • ok(value) / err(error) — construct each side.
  • tryOr(fn, toError) — run a function and capture any thrown error as the Err side. This is the boundary where an unexpected exception (for example an aborted fetch) becomes a LiganticError.

Reading a LiganticError

LiganticError mirrors the API's documented error envelope { message, code, issues? } and adds transport context:

Field

Meaning

message

Human-readable description.

status

HTTP status, or null when the request never completed (network error, abort).

code

The API error code, for example, "NOT_FOUND", "UNAUTHORIZED", "CONFLICT".

issues

Validation issues, present for 422-style responses.

requestId

The x-request-id header, when the server provides one — include it in support reports.

isTransportError

true when status === null.

isRetryable

true for transport errors, 408, 429, or 5xx.

A few examples:

// API returned 404.
error.status; // 404
error.code; // "NOT_FOUND"
error.message; // "..."

// The request never reached the server.
error.status; // null
error.code; // "NETWORK_ERROR"
error.isTransportError; // true

How This Interacts with Retries

The retry policy is idempotent-only and keyed off isRetryable:

  • Only GET, DELETE, and PUT are auto-retried.
  • A request is retried on a transport failure or a 408/429/5xx.
  • POST and PATCH are never auto-retried — a non-idempotent failure is surfaced immediately as an Err result so you can decide what to do.

So by the time you inspect a result, any retryable idempotent failure has already been retried up to the configured maxRetries.

Handling a Failed Result

const result = await client.flows.run(spaceId, flowId, { inputs });

if (!result.ok) {
  const e = result.error;
  // Log or surface. Include e.requestId for support.
  throw new Error(`${e.code}: ${e.message}${e.requestId ? ` (request ${e.requestId})` : ""}`);
}

const runId = result.value.flowExecutionId;

For the CLI, you don't write this at all — the CLI renders the Err side as error: <message> on stderr and exits non-zero.

Related pages