Vereda
    Preparing search index...

    Results and errors

    What a finished request gives you: typed, validated bodies and a typed set of error classes. Back to the README.

    Pass a parse function to validate and type the response body. parse is just (data: unknown) => T | Promise<T>, and any validator that throws (or rejects) on failure works. A failed parse resolves the ticket with a ValidationError and is never retried. With parse set, so does a body that isn't valid JSON, including an empty one (a 204, or any HEAD response): the server answered, and asking again would get the same answer.

    const ticket = client.get<User>("/users/1", {
    parse: (data) => data as User, // or your own throwing validator
    });

    const result = await ticket.toPromise();
    if (result.success) {
    result.data.name; // typed: string
    }

    json<T>() is the built-in, dependency-free version of that cast.

    parse runs inside the attempt, so waiting for an async parse counts against attemptMs and totalMs. A parse that never settles ends as a timeout, and cancel() ends it too.

    responseType sets how the body is read before parse sees it. It defaults to "json" when parse is set. Without parse, data is the body read that way:

    responseType data without parse
    "json" unknown (decoded, not validated)
    "text" string
    "arrayBuffer" ArrayBuffer
    "blob" Blob (typed with the response's content-type)
    "none" undefined; the body is cancelled unread
    const csv = await client.get("/export.csv", { responseType: "text" }).toPromise();
    if (csv.success) csv.data; // string

    await client.delete("/users/1", { responseType: "none" }).toPromise(); // 204: nothing to read

    The body is read inside the attempt, so attemptMs and totalMs bound the read. With neither parse nor responseType, the body is left unread on result.raw and data is undefined.

    Zod is an optional peer dependency. Only the @vereda/http/zod entry point imports it; the core has zero dependencies. Vereda ships a Zod adapter for it. It validates with parseAsync, so async refinements and transforms work:

    import { z } from "zod";
    import { withZod } from "@vereda/http/zod";

    const UserSchema = z.object({
    id: z.number(),
    name: z.string(),
    email: z.email(),
    });

    const ticket = client.get("/users/1", { parse: withZod(UserSchema) });

    const result = await ticket.toPromise();
    if (result.success) {
    result.data.name; // typed: string
    }

    Errors form a hierarchy under RequestError, and AppError is the union of all of them: a failure is always one of these classes, never an arbitrary thrown value. Every class carries a readonly, literal-typed kind, so switching on it narrows to that class and its fields (instanceof works too):

    Error kind Meaning Notable fields
    NetworkError "network" Request failed before a response arrived (DNS, connection reset, etc.) cause
    HttpError "http" Non-2xx response outside retry.retryOnStatus (e.g. 404) statusCode, response
    RetryableStatusError "retryable_status" Non-2xx response matching retry.retryOnStatus (e.g. 503) statusCode, response (status and headers only; its body was cancelled), retryAfterMs?
    TimeoutError "timeout" Attempt exceeded timeout.attemptMs url, timeoutMs
    DeadlineExceededError "deadline" Ticket exceeded timeout.totalMs, or its next retry delay wouldn't fit before it (terminal — not retried) url, totalMs, cause (last attempt's error, when a retry was skipped)
    ValidationError "validation" Response body failed parse or isn't valid JSON (terminal — never retried) issues, cause
    CancelledError "cancelled" Ticket cancelled or signal aborted (terminal) —
    QueueFullError "queue_full" A partition's queue (scope: "partition"; retries, plus first attempts with limitFirstAttempts), or the global queue (scope: "global"), was full (terminal) scope, partition ("global" for the global queue; check scope, since a partition can be named that too), queueSize, maxQueueSize
    CircuitOpenError "circuit_open" The partition's breaker was open when an attempt was due, so it wasn't sent; earlier attempts may have run (terminal) partition
    RateLimitedError "rate_limited" The partition's rate limit would have made an attempt wait longer than maxWaitMs, so it wasn't sent; earlier attempts may have run (terminal) partition, waitMs, maxWaitMs
    ConfigurationError "configuration" A relative URL with no baseUrl, a bare ReadableStream body, a body factory that threw, or invalid request-level timeout/retry options (terminal). Invalid client config throws from create() instead key
    MaxRetriesExceededError "max_retries" Retries ran out while the failure was still transient (terminal) attempts, lastError

    Only network, timeout, and retryable_status are retried by default — see What gets retried above. Everything else is terminal: it resolves the ticket on the first attempt that produces it.

    TimeoutError and DeadlineExceededError can also arrive outside a Result: as the rejection of a body read performed later, on result.raw or HttpError.response (see the hero example above) — not only as result.error.

    const result = await ticket.toPromise();
    if (!result.success) {
    switch (result.error.kind) {
    case "max_retries":
    result.error.lastError; // MaxRetriesExceededError: the final attempt's error
    break;
    case "http":
    result.error.statusCode; // HttpError: also .response
    break;
    case "circuit_open":
    result.error.partition; // CircuitOpenError: the partition that is failing fast
    break;
    default:
    console.error(result.error.message);
    }
    }

    Keep a default branch like the one above. A minor release may add an error class with a new kind (see Versioning and support), and a switch that ends in a const unreachable: never = result.error check would stop compiling when one arrives. The default branch handles it at run time instead.