Errors
@flue/sdk exports three error classes of its own — FlueApiError (a failed HTTP request), FlueExecutionError (an admitted submission that settled failed or aborted), and UnsupportedFlueEventVersionError (an event-format version mismatch) — plus two error-shape types (FluePublicError, FlueSerializedError) and four re-exported stream error classes owned by @durable-streams/client.
Every class sets name to its class name. Discriminate with instanceof; message strings are composed for logging and are not API.
import { createFlueClient, FlueApiError, FlueExecutionError } from '@flue/sdk';
const conversation = createFlueClient({ url: 'https://example.com/agents/triage/123456' });
try {
const admission = await conversation.send({ message: { kind: 'user', body: 'Hello' } });
await conversation.wait(admission);
} catch (error) {
if (error instanceof FlueApiError) {
// The HTTP request was rejected: error.status, error.body.
} else if (error instanceof FlueExecutionError) {
// The message was admitted, but the submission settled failed or aborted.
} else {
throw error;
}
}
FlueApiError
class FlueApiError extends Error {
readonly status: number;
readonly body: unknown;
constructor(status: number, body: unknown);
}
Rejection value of every SDK JSON request that returns a non-2xx response: send(), abort(), and history(). The SDK performs exactly one fetch per JSON request — no retries — so a FlueApiError reflects a single server response.
| Field | Description |
|---|---|
status |
The HTTP response status. |
body |
The parsed JSON response body when it parses; the raw response text when it does not; the empty string when the response had no body. Deliberately unknown: proxies and gateways in front of a deployment can return arbitrary bodies, so the SDK does not normalize. |
The message is composed from the status and, when the body carries the Flue error envelope, the envelope’s type and message (for example Flue API error 404 [agent_instance_not_found]: Agent instance "123456" was not found.). Match on status and body, not on the string.
Two methods never produce FlueApiError:
wait()reads a durable stream rather than making JSON requests; its transport failures are the stream errors below.observe()never throws at all; aFlueApiErrorfrom its internal history reads lands on the observation snapshot instead (see Errors inobserve()).
The HTTP error envelope
Every error response the Flue runtime renders itself carries one JSON envelope: { "error": FluePublicError }.
interface FluePublicError {
type: string;
message: string;
details: string;
dev?: string;
meta?: Record<string, unknown>;
}
| Field | Description |
|---|---|
type |
Stable, machine-readable identifier (snake_case, e.g. agent_instance_not_found). This is the field to branch on; message and details wording is not API. |
message |
One-sentence summary, safe to show to any caller. |
details |
Longer caller-safe explanation. Always present (possibly empty). |
dev |
Developer-audience guidance (fix instructions, configuration hints). Present only when the server runs in local development mode and the error has dev-only guidance; its absence is not a reliable prod signal. |
meta |
Optional structured data, set only where downstream tooling benefits. |
FlueApiError.body holds the whole envelope, so the type is reached as (error.body as { error: FluePublicError }).error after checking the shape. The server-side error vocabulary — which type values exist and their statuses — is documented in the runtime errors reference. Two rejections tied to send()’s uid condition are worth knowing here:
404agent_instance_not_found— auid-conditioned send named an instance that does not exist or whose uid no longer matches (the instance was re-created and the uid names its previous incarnation); nothing was delivered. The two cases are deliberately indistinguishable.409agent_instance_exists— a create-only send (uid: null) named an existing instance;detailshands back the existing uid.
Errors thrown by application middleware or non-Flue infrastructure are not enveloped; that is why body stays unknown.
FlueExecutionError
type FlueExecutionTarget = 'agent_submission';
type FlueExecutionFailure = 'failed' | 'aborted' | 'terminal_event_missing';
class FlueExecutionError extends Error {
readonly target: FlueExecutionTarget;
readonly targetId: string;
readonly failure: FlueExecutionFailure;
readonly error: unknown;
constructor(options: {
target: FlueExecutionTarget;
targetId: string;
failure: FlueExecutionFailure;
error?: unknown;
});
}
Rejection value of wait(): the message was admitted and executed, but the submission settled with an outcome other than completed. A FlueApiError means the request never got in; a FlueExecutionError means the agent ran and did not finish successfully.
| Field | Description |
|---|---|
target |
What was being awaited. The union has a single member today, 'agent_submission'. |
targetId |
The awaited submission’s submissionId (from the AgentSendResult admission). |
failure |
'failed' when the submission settled failed; 'aborted' when it settled aborted (for example after abort()); 'terminal_event_missing' when the conversation stream ended without a terminal settlement event for this submission. |
error |
The settlement’s error payload, when the settlement carried one; undefined for terminal_event_missing. Typed unknown because the value crosses the wire, but errors the runtime itself serializes follow FlueSerializedError. |
interface FlueSerializedError {
name?: string;
message: string;
type?: string;
details?: string;
dev?: string;
meta?: Record<string, unknown>;
}
type, details, and meta are present when the underlying failure was a typed runtime error (the same identity vocabulary as the runtime errors reference); plain errors serialize as name and message only.
wait() distinguishes server-side abortion from caller-side cancellation: a conversation aborted via abort() rejects with FlueExecutionError (failure: 'aborted'), while firing the caller’s own AbortSignal rejects with the signal’s reason — a DOMException named AbortError when the signal carries no reason — and is not a FlueExecutionError. See Aborts are not SDK errors.
UnsupportedFlueEventVersionError
class UnsupportedFlueEventVersionError extends Error {
readonly received: unknown;
readonly supported = 3;
constructor(received: unknown);
}
Thrown while iterating a FlueEventStream of FlueEvent values when a delivered event’s version marker v is not 3. SDK readers do not normalize historical event formats — v1/v2 data written by an earlier Flue beta must be cleared, not migrated on read.
| Field | Description |
|---|---|
received |
The version value found on the offending event (possibly undefined). |
supported |
Always 3. |
The throw is terminal for the stream: the underlying connection is cancelled and subsequent next() calls rethrow the same error.
The conversation wire (wait(), observe()) validates differently: each chunk is checked against the materialized-conversation protocol, and a protocol mismatch raises an internal ConversationStreamError (name: 'ConversationStreamError', not exported). observe() recovers from it by rehydrating a fresh snapshot; from wait() it propagates as a rejection.
Stream errors
wait(), observe(), and FlueEventStream iteration read durable streams. Transport and protocol failures on those reads come from @durable-streams/client; the SDK re-exports the classes reachable through its read paths so applications can discriminate them without depending on that package directly. Their shapes are owned by that package and track its releases.
DurableStreamError
class DurableStreamError extends Error {
code:
| 'NOT_FOUND' | 'CONFLICT_SEQ' | 'CONFLICT_EXISTS' | 'BAD_REQUEST'
| 'BUSY' | 'SSE_NOT_SUPPORTED' | 'UNAUTHORIZED' | 'FORBIDDEN'
| 'RATE_LIMITED' | 'ALREADY_CONSUMED' | 'ALREADY_CLOSED'
| 'PARSE_ERROR' | 'STREAM_CLOSED' | 'UNKNOWN';
status?: number;
details?: unknown;
}
Protocol-level stream failure — a malformed or unparseable stream response, an SSE upgrade the server does not support, or an HTTP status mapped to a structured code.
| Field | Description |
|---|---|
code |
Structured code for programmatic handling (the union above; the DurableStreamErrorCode type alias itself is not re-exported). |
status |
HTTP status, when the failure maps to a response. |
details |
Additional data, typically the raw response body. |
StreamClosedError
class StreamClosedError extends DurableStreamError {
readonly code = 'STREAM_CLOSED';
readonly status = 409;
readonly streamClosed = true;
readonly finalOffset?: string;
}
DurableStreamError subclass reporting an operation against a stream that has already been closed. finalOffset carries the stream’s final offset when the response provided one.
FetchError
class FetchError extends Error {
status: number;
text?: string;
json?: object;
headers: Record<string, string>;
url: string;
}
A stream HTTP request that failed without a protocol-level classification. The stream layer retries transient failures with exponential backoff (429, 503, all 5xx, and network errors; by default indefinitely — bound it via the backoffOptions accepted by wait() and observe()), so with default options a FetchError surfaces for non-retryable client errors: 4xx other than 429.
| Field | Description |
|---|---|
status |
HTTP status of the failed request. |
text / json |
The response body, as text or parsed JSON depending on the response content type. |
headers |
The response headers. |
url |
The requested URL. |
FetchBackoffAbortError
class FetchBackoffAbortError extends Error {}
A stream request was abandoned because its signal aborted during retry backoff. No extra fields. Most caller-initiated aborts do not surface this class — SDK stream iteration ends quietly on the caller’s own signal, and wait() then rejects with the signal’s reason — but it can appear where the stream layer’s abort races its retry loop.
Errors in observe()
observe() neither throws nor rejects. Failures surface on the observation snapshot (getSnapshot().phase and .error):
- A
404from the initial history read setsphase: 'absent'with no error — the conversation does not exist yet. - A
400,401, or403(from any error value carrying a numericstatus, such asFlueApiErrororFetchError) is fatal:phase: 'error'with the error onsnapshot.error. No further retries. - Every other failure — network errors, 5xx, a stream that ends unexpectedly — schedules a rehydrate with exponential delay (1 s doubling, capped at 30 s):
phase: 'connecting'with the pending error onsnapshot.error. - Closing (via
close()or thesignaloption) setsphase: 'closed'.
The full observation contract is on the client page.
Aborts are not SDK errors
Cancellation surfaces as the abort reason, never as a Flue error class:
send(),abort(), andhistory()reject with whatever the fetch implementation throws for an aborted request — aDOMExceptionnamedAbortErrorunder the standardfetch.wait()rejects withsignal.reason, or aDOMExceptionnamedAbortErrorwhen the signal carries no reason.FlueEventStreamiteration does not throw on cancellation:cancel()(or breaking out offor await) ends iteration withdone: true.
Check error.name === 'AbortError' (or your own signal.reason) before treating a rejection as a failure.
Construction errors
createFlueClient() throws synchronously — with native errors, not an SDK class — when the conversation URL cannot be resolved:
- A relative
urloutside a browser throwsTypeError: relative url requires a browser; pass an absolute URL. In a browser, relative URLs resolve againstlocation.origin. - A
urlthat is not a valid URL throws the nativeTypeErrorfrom theURLconstructor.
No network activity happens at construction; everything else fails at call time through the classes above.