Errors & pitfalls
How StitchAPI errors work, how to tell failures apart at runtime, and an index of every documented failure.
When a stitch can't produce a result, it fails with a StitchError — an Error
subclass exported from stitchapi, carrying a human-readable message, an
optional .status (the upstream HTTP status), and .attempts (how many tries
the runtime made before giving up). When the failure came from an HTTP response it
also carries .body (the parsed error payload — an API's { error: "…" }) and
.url (the final request URL, after redirects); both are undefined for
transport/internal errors. Await a stitch and that error is what catch receives —
narrow it with instanceof:
import { , } from 'stitchapi';
const = ({
: 'https://api.example.com',
: '/me',
});
try {
await ();
} catch () {
if ( instanceof ) {
// .body is the parsed error payload; .url is the final request URL.
.(., ., ., ., .);
}
}.body carries the upstream error payload only to the awaited / .safe()
caller — it rides a non-enumerable channel and is never written to a trace
sink, so an { error: "…" } payload can't leak into a JSONL/console log. If a
non-2xx is actually expected control flow (a 404 you fall back from), see
verdict.accept — it returns the body
as a normal result instead of throwing.
If you read the event stream instead of awaiting,
the same failure arrives as an error event carrying
{ name, message, status?, attempts, at } — attempts is how many tries the
runtime made before giving up, at is when it gave up.
Handle failure without throwing
await and .unwrap() throw on failure. When you'd rather branch than wrap the
call in try/catch, .safe() consumes the call and never throws — it resolves
to a discriminated { ok, data, error }, where error is the same StitchError,
or null on success:
import { } from 'stitchapi';
const = ({ : 'https://api.example.com', : '/me' });
const { , } = await .();
if () .(., .);
else .(); // the validated result; error is null here.unwrap() is the explicit throwing twin of .safe() — the value, or a thrown
StitchError, exactly like awaiting the bare call. See
the event stream for the iterate-instead-of-await
view of the same outcomes.
Catalog IDs are documentation, not runtime values
Each catalog page below is filed under a stable catalog ID
(STITCH_VALIDATION, STITCH_DRIFT, and so on), and the ID's page slug is its
URL. Slugs are an API: once a page is published its slug is never renamed, only
added-and-redirected, so a link written a year ago still resolves.
These IDs name documentation pages — they are not values the runtime
puts on the error. A thrown StitchError has no .code property, so
err.code is undefined and if (err.code === 'STITCH_DRIFT') never
matches. Use the IDs to refer to a failure mode (in this catalog, in the
docs MCP, in an issue report); to branch in code, use the fields
below.
Telling failures apart at runtime
Every failure arrives as a StitchError except the delegate-backoff rate limit,
which is its own exported RateLimitError. So narrow with instanceof first,
then branch on .status and .attempts — together they separate every failure
mode in the catalog:
| What you see | What happened |
|---|---|
attempts === 0 | No request was made — input validation (status undefined) or an open circuit (status 503). |
status is a 2xx | The response arrived and broke its contract — an output schema/drift failure, or GraphQL errors. |
status is non-2xx | The upstream rejected it, after any retries — a 401 auth wall, a 5xx, and so on. |
status undefined, attempts ≥ 1 | The request went out but never produced a response — a timeout or a transport error. |
import { , , } from 'stitchapi';
const = ({ : 'https://api.example.com', : '/report' });
try {
await ();
} catch () {
if ( instanceof ) {
// The one catalog entry that IS a runtime identity of its own. It
// subclasses StitchError, so this arm MUST come before the one below —
// otherwise the generic arm swallows it.
.('back off for', .);
} else if ( instanceof ) {
if (. === 0) {
// Nothing left the process: bad input, or the breaker is open.
.(. === 503 ? 'circuit open' : 'bad input');
} else if (. !== && . < 300) {
// A 2xx that still failed: the body broke the declared contract.
.('contract violation', .);
} else {
.('upstream failed', . ?? 'no response');
}
}
}.message is human-readable, not a contract — today it is
invalid <slot>: <issue> for input validation,
contract violation (drift) for a response that failed output,
timed out after <n>ms, circuit open, GraphQL: <message>, or
HTTP <status>. Match on it only as a last resort, and expect the wording
to change.
Two catalog IDs share one runtime failure: a plain output schema failure
(STITCH_VALIDATION) and a hard drift()
failure (STITCH_DRIFT) both throw the same contract violation (drift)
StitchError. The thrown error does not say which path broke — that detail
rides the drift events on the event stream,
where a hard failure appears as a finding with level: 'error' and
change: 'invalid':
import { , } from 'stitchapi';
import { } from 'zod';
const = ({
: 'https://api.example.com',
: '/user',
: (.({ : .() })),
});
for await (const of ().()) {
if (. === 'drift' && .. === 'error') {
// The structured signal: which path broke, and how.
.(.., .., ..);
}
}Every documented failure
Debugging one of these with an agent? Connect the docs
MCP and it can pull any error page on demand
— search_docs('STITCH_DRIFT'), search_docs('rate limit') — instead of
guessing from stale training data.
STITCH_VALIDATION
A response failed its output schema — the shape you asked for isn't the shape you got.
STITCH_DRIFT
A response drifted from the schema past the level you allowed, so the stitch refused it.
STITCH_AUTH_WALL
Authentication failed, or a soft 200 login wall was hit and couldn't be refreshed.
STITCH_TIMEOUT
A call exceeded its timeout budget before a response arrived.
STITCH_CIRCUIT_OPEN
The circuit breaker is open after repeated failures and is short-circuiting calls.
STITCH_GRAPHQL
A GraphQL-over-HTTP response came back 200 but carried an errors
array.
RateLimitError
A delegate-backoff stitch surfaced a rate-limit response for an outer gate to back off on, instead of retrying it.