Release candidate — 1.0.0-rc.7
StitchAPI

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 seeWhat happened
attempts === 0No request was made — input validation (status undefined) or an open circuit (status 503).
status is a 2xxThe response arrived and broke its contract — an output schema/drift failure, or GraphQL errors.
status is non-2xxThe upstream rejected it, after any retries — a 401 auth wall, a 5xx, and so on.
status undefined, attempts1The 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.

See also

On this page