paramour
Referenceparamour (core)

Errors

The ParamourError hierarchy, brand-based instanceof, and the Issue shape shared by every failed decode.

Every error paramour throws is a ParamourError. Foreign throws — a Standard Schema validator that throws instead of returning issues, a .default() factory that fails, a props promise that rejects — are caught at chokepoints and rebranded into the hierarchy, with the original attached as cause. So one instanceof ParamourError check in an error boundary catches everything the library can produce.

ParamourError
├─ ParseError          one wire value failed its codec grammar or schema
├─ SerializeError      a value could not be serialized to the wire
├─ ParamsDecodeError   aggregate params-decode failure — carries issues
├─ SearchDecodeError   aggregate search-decode failure — carries issues
└─ SearchSourceError   a source violated the wire-shape contract — carries key

instanceof is hardened with cross-copy identity brands: each class brands its prototype with a Symbol.for() symbol, which resolves in the realm-global symbol registry — so a second physical copy of the module (dual-package hazard, bundler duplication) mints the same symbols, and instanceof recognizes instances across copies. A structurally identical foreign class lacks the brands entirely and never passes. Each class checks its own brand (a ParseError check never matches a plain ParamourError), and the checks are typed as predicates, so TypeScript narrows through them.

ParamourError

The base class — and the type thrown directly for contract violations: an invalid path literal at define time, a hand-built route missing a codec, a config or source that isn't even an object, a rebranded foreign throw. These are programming errors, so they deliberately stay loud — the safe* APIs never soften them into the error arm.

ParseError

A single wire value failed its codec grammar or schema validation — "abc" is not an integer, "2026-13-40" is not a date. This is the element-level error, and it is exactly what .catch() recovers: inside the high-level decoders every ParseError is either caught into the codec's fallback or folded into the aggregate's issues[], so you normally meet it as an issue message rather than a raw throw. It surfaces directly only on low-level paths (custom codec internals, tooling using paramour/internal).

SerializeError

A value could not be serialized to the wire: the wrong runtime type, a value failing its codec or schema on serialize, a required param or search key missing at encode time, a required catch-all given [], a segment serializing to "", a custom serializer returning a non-string, or unencodable text (lone surrogates). Thrown by the whole encode surface — href, buildPath, encodeParams, encodeStaticParams, encodeSearch, searchToString, buildSearchString, serializeValue — at link-build time, in the component that introduced the bad value.

ParamsDecodeError

The aggregate failure for a whole params decode. Carries issues, one entry per failed key — a decode never stops at the first problem:

import {
  ,
  ,
  ,
  ,
  ,
} from "paramour";

const  = ("/product/[id]", {
  : { : .() },
});

try {
  (, { : "abc" });
} catch () {
  if ( instanceof ) {
    .issues;
ParamsDecodeError.issues: readonly Issue[]
} else if ( instanceof ) { // contract violation — a bug in the caller, not a bad URL throw ; } }

Thrown by decodeParams, route.parseParams, and the params half of route.parse / route.parseContext; returned in the error arm by their safe* twins.

SearchDecodeError

The aggregate failure for a whole search decode — same shape, same issues property, so error rendering written for one surface works for both. Thrown by decodeSearch, route.parseSearch, the search half of route.parse / route.parseContext, and a failed rawSearch schema (whose root-level issues appear under the key "<search>"); standardSearchSchema converts it to spec-shaped issues instead of throwing.

The message is the pretty printout

Both aggregate errors also carry route — the owning route's path pattern, or null when the decode ran outside a route (a bare decodeSearch call) — and their message renders the whole failure, one line per issue:

Failed to decode search params for /users/[id]:
  ✖ page: required search param is missing (expected integer)
  ✖ sort: "up" is not one of: asc, desc

There is no separate prettify step on purpose: an unhandled decode error reaches a Next error boundary or the dev overlay as error.message, so the default message is the one tuned for that moment. (expected …) is keyed on the issue's structured reason: it is appended exactly where the message cannot name the expected shape itself — a "missing" key has no value to describe, and a "validate" failure carries validator prose that may name neither the value nor the grammar. Core's own "parse" grammar messages already quote the value and name the grammar, and a "duplicate" or "shape" message states a problem that isn't about the grammar at all, so none of those take the suffix.

SearchSourceError

A search source violated its wire-shape contract: the source isn't an object, or a declared key's value isn't a string / string[]. Carries key — the offending source key, or null when the source itself is malformed. This is distinct from a decode failure on purpose: a malformed source is the caller's bug, so it stays loud everywhere — including through safeDecodeSearch — with one exception: standardSearchSchema's validate() receives genuinely untrusted input, so exactly these errors soften to spec issues at that one boundary.

Which API throws what

APIFailure
decodeParams, route.parseParamsParamsDecodeError
decodeSearch, route.parseSearchSearchDecodeError; SearchSourceError for a contract-violating source
route.parse, route.parseContextParamsDecodeError or SearchDecodeError — params decode first
href, buildPath, encodeParams, encodeStaticParams, encodeSearch, searchToString, buildSearchStringSerializeError
defineAppRoute, definePagesRouteParamourError on an invalid path literal
any API given a config or source that breaks its documented shape (plain-JS callers)base ParamourError

The safe* and safeDecode* variants return the two aggregate decode errors in their error arm instead of throwing; everything else in this table still throws through them.

RouteDecodeError is the union of the two aggregate errors — the type of SafeResult's error arm:

type RouteDecodeError = ParamsDecodeError | SearchDecodeError;

Issue is one failed key in an aggregate error — the same shape on both surfaces (and in the hooks and devtools), so issue rendering is written once:

interface Issue {
  readonly expected?: string;
  readonly key: string;
  readonly message: string;
  readonly reason?: IssueReason;
  readonly wire?: string;
}

type IssueReason = "duplicate" | "missing" | "parse" | "shape" | "validate";

message is the human prose; expected, reason, and wire are its structured halves for custom rendering. reason says what kind of failure the issue records — "missing" (no wire value for a required key), "duplicate" (multiple values for a single-value param), "shape" (source shape mismatch: an array where a segment belongs, a non-string element), "parse" (the codec's own wire grammar rejected the value), or "validate" (user-supplied code rejected it: a Standard Schema validator or a custom codec's parse). Core always sets it; it is optional only so prose-only issues built outside core remain representable. expected is the codec's bare shape label (integer, enum(asc|desc), csv<integer>[]formatCodecDescription's "shape" style), absent when no codec owns the key (a rawSearch schema issue). wire is the offending value as the codec grammar saw it — the value-layer string after percent-decoding, not the raw URL text (search sources arrive platform-decoded; route segments are decoded by core first, so a segment 1%20x records wire: "1 x"). Absent when there isn't exactly one offending value: a missing key, a non-string source value, or the duplicate-scalar rejection.

On this page