Uh oh!
There was an error while loading. Please reload this page.
docs(api): narrow the client-SDK error-handling examples' catch binding - #12387
Conversation
The two Error Handling examples on content/docs/api/client-sdk.mdx read error.code / error.httpStatus / error.fields straight off an untyped catch binding. Under strict (which implies useUnknownInCatchVariables, the tsc --init default since TS 4.4) that binding is unknown, so a reader copying either block into their own project gets TS18046 on every read -- measured, 5 in the first block and 2 in the second. Both blocks now narrow through a guard the first one declares. The shape is read from what the client actually attaches at its request seam (packages/client/src/index.ts): a real Error carrying httpStatus (always, for a server response), plus code / category / retryable / details / fields when the server sent them. Deliberately NOT the exported StandardError interface: that type describes the server's error ENVELOPE, its code is the closed StandardErrorCode enum which does not contain the ledger-registered VALIDATION_FAILED the examples branch on, and it declares no fields member at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
yinlianghui
commented
Aug 26, 2026
ACCEPT — devx@objectstack seat (#6023), R9
Zone 2b said the narrowing had to come from what the SDK throws, not from what the card assumed. It did, and it found the better discriminatorI flagged ⭐ And the refusal is the sharper half. Narrowing to the exported The examples branch on
Zone 1.2's tension was resolved out loud, in the better placeI ruled that you may not add You said it in the page's own header, not just the PR body:
That is where the next editor actually reads, which is better than where I asked for it. You also corrected the header's now-stale claim (the blocks used to add TS18046) and named the one remaining unnarrowed RecordedThe type guard is declared in the first Error Handling block and reused by the second, so the page teaches one guard rather than repeating a cast — and ⛔ No governed surface, ⛔ not Generated by Claude Code |
Fixes#12342
The two Error Handling examples on
content/docs/api/client-sdk.mdxreaderror.code/error.httpStatus/error.fieldsstraight off an untypedcatchbinding. Understrict— which impliesuseUnknownInCatchVariables,the
tsc --initdefault since TS 4.4 — that binding isunknown, so a readercopying either block into their own project gets
TS18046on every read.Both blocks now narrow through a guard the first block declares, in the same
continuation style the page already uses for
client.Nothing on this page compiles these two blocks.
check:skill-examplesonlytype-checks fences carrying an
os:checkmarker, and this PR deliberatelyadds none — marker coverage for this page is its own separate card and is not
in scope here. So the corrected examples are exactly as unchecked by CI as the
broken ones were. Please do not read the green checks below as "the examples now
compile"; they say nothing about these blocks.
What stands behind the fix instead is a local measurement, reproduced below,
against a tsc profile copied from the gate's own (
strict: true,target ES2020,libES2020 + DOM,types: [],moduleResolution: bundler— see thetsconfig
packages/spec/scripts/check-skill-examples.tswrites).Before — the two blocks as they stood
5 + 2 — the exact TS18046 counts the card measured.
clientandshowFieldErrorwere declared in the harness, so the page's separate TS2304continuation convention is excluded and only the diagnostics under test remain.
After — the same two blocks, extracted from the committed file
tsc --noEmitexits0, no diagnostics. Block 1 is self-contained apart fromclient; block 2 additionally reads block 1's guard, exactly as it alreadyreads block 1's
client.What the SDK actually throws — read, not assumed
The narrowing is written from the client's own request seam
(
packages/client/src/index.ts, the!res.okbranch), which is where everyserver-response failure is constructed:
Error(new Error(errorMessage))httpStatuserror.httpStatus = res.status, unconditionalcodecodeorerror.code; otherwiseundefinedcategory,retryableerror.category/error.retryabledetailsfieldsSo
httpStatusis the honest discriminator: it is on every error thrown for aserver response and on none of the errors the client throws before there is one
(
Streaming response carried no body,project(id): environmentId is required,Storage Upload Failed). Those keep propagating, which is why theexample rethrows rather than swallowing.
Why not the exported
StandardErrorStandardErroris exported from@objectstack/client, and narrowing to it wasthe obvious route. It is the wrong shape, on three measured counts:
codeisStandardErrorCode, a closed enum that does not containVALIDATION_FAILED— that code lives inERROR_CODE_LEDGER(
packages/spec/src/api/error-code-ledger.zod.ts), andVALIDATION_FAILEDis precisely the value both examples branch on. Typing
codeasStandardErrorCodewould turn the page's own comparison into a compileerror.
fieldsmember, andfieldsis the entire subject of thesecond block.
categoryandretryable, which the flat REST envelope nevercarries — the thrown object leaves both
undefined.StandardErrordescribes the server's error envelope; it is not a claimabout the object the client throws. The page now says so, so the next reader
does not repeat the attempt.
No new export was added. The SDK still ships no error class and no type
guard; the guard in the example is one a reader writes in their own code. If a
supported guard should exist, that is a public-surface question and belongs in
its own card.
Scope
Declared surface was the two error-handling examples. Also touched, both
adjacent and both declared here rather than left silent:
error-handling blocks additionally read a
catchbinding that isunknown(TS18046). Making those compile would mean hand-declaring the SDK's own types
… teaching worse code than the page teaches now." That is now false about
these two blocks, and a stale rationale sitting above the fences it describes
is a trap for the next author. Rewritten to say what is true now, including
that the blocks are still unmarked.
error.codeis always the semantic code as astring" — measurably not always:
errorCodeisundefinedwhen the serversent neither spelling. Softened to "whenever the server sent one", because the
new interface types it
code?: stringand the two would otherwise contradicteach other on the same screen.
A third block on this page has the same defect and is left alone: the
automation.executeexample inside the API-surface tour fence (around line 431)reads
err.httpStatus/err.code/err.details?.…off an untyped binding —4 more would-be TS18046. It sits inside a large multi-section tour fence, is not
one of the two blocks this card names, and is a fragment for other reasons. Not
touched here; worth its own card.
Verification
Gate union re-derived on the final tree with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(noSTALE TREE), then all 23 matched families run atd87b4d5ecf, exit codescaptured before any pipe. All 23 green, including
pnpm --filter @objectstack/spec run check:skill-examples, which reports✅ 260 prose examples type-check across 3 surface(s).Repo-wide
pnpm lintwas not run, and it could not have said anything:asked through eslint's own config resolution, this diff's single file is
outside its population entirely —
So the lintable-file count in this diff is 0, and with no config applying to the
path there is no type-aware linting that could move an untouched file's verdict.
No changeset:
content/is onchangeset-check's own "releases nothing" list,so this PR carries the
skip-changesetlabel instead.Generated by Claude Code