From d87b4d5ecf571522f422d0be0da845600277b80f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 23:50:19 +0000 Subject: [PATCH] docs(api): narrow the client-SDK error-handling examples' catch binding 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 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- content/docs/api/client-sdk.mdx | 55 +++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx index 032275974a..047619946e 100644 --- a/content/docs/api/client-sdk.mdx +++ b/content/docs/api/client-sdk.mdx @@ -43,11 +43,14 @@ pnpm add @objectstack/client React Hooks block — the three that stand alone. What is not: every block that continues Quick Start's implied context. Quick Start establishes `client` once and each later block reads it, so a marker there reds with TS2304 - "Cannot find name 'client'"; the error-handling blocks additionally read a - `catch` binding that is `unknown` (TS18046). Making those compile would mean - hand-declaring the SDK's own types or injecting casts into prose whose - subject IS the real API — pinning each example to itself and teaching worse - code than the page teaches now. + "Cannot find name 'client'". The two Error Handling blocks USED to add + TS18046 ("'error' is of type 'unknown'") on top of that; they now narrow the + `catch` binding through a guard the first block declares, which is what a + reader's own strict project needs anyway. That leaves only the page-wide + TS2304 convention between them and a marker — still unmarked here because + the marker question is its own card, so nothing compiles these two blocks. + The remaining unnarrowed `catch` is inside the API-surface tour block + (`automation.execute`), which is a fragment for other reasons. Measured with all 13 fences marked, then reverted: 114 diagnostics, TS2304 / TS18046 / TS18004 / TS2591, spread over the nine continuation blocks — and @@ -584,12 +587,35 @@ message in `errors[0].message`), records ride `row.data` when ## Error Handling -All errors follow a standardized format: +All errors follow a standardized format. The SDK throws a real `Error` with +those fields attached to it, so under `strict` (which implies +`useUnknownInCatchVariables`) the `catch` binding is `unknown` and you must +narrow before reading them — the SDK exports no type guard of its own, so +declare the shape you rely on and test for it: ```typescript +/** + * What the SDK attaches to the `Error` it throws for a non-2xx response. + * `httpStatus` is always set; the rest are present only when the server + * sent them. + */ +interface ObjectStackApiError extends Error { + httpStatus: number; + code?: string; + category?: string; + retryable?: boolean; + details?: unknown; + fields?: { field: string; code: string; message: string }[]; +} + +function isApiError(error: unknown): error is ObjectStackApiError { + return error instanceof Error && 'httpStatus' in error; +} + try { await client.data.create('todo_task', { subject: '' }); } catch (error) { + if (!isApiError(error)) throw error; // not a server response — rethrow console.error(error.code); // 'VALIDATION_FAILED' console.error(error.httpStatus); // 400 console.error(error.category); // optional — set only when the server sent it @@ -598,10 +624,18 @@ try { } ``` -`error.code` is always the **semantic** code as a string — the numeric HTTP -status lives on `error.httpStatus` and nowhere else. This holds regardless of -which server surface answered: the REST server replies with a flat -`{ error, code, fields }` body and the runtime dispatcher replies with a wrapped +`httpStatus` is the discriminator because the client sets it on **every** error +it throws for a server response, and on none of the errors it throws before one +(a runtime without `Response.body`, a missing `environmentId`). Do not narrow to +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` these examples branch on), +and it carries no `fields`. + +`error.code` is the **semantic** code as a string whenever the server sent one — +the numeric HTTP status lives on `error.httpStatus` and nowhere else, and is set +even when no code was. This holds regardless of which server surface answered: +the REST server replies with a flat `{ error, code, fields }` body and the runtime dispatcher replies with a wrapped `{ success, error: { code, message, httpStatus, details } }` body, and the client normalizes both before throwing. (Dispatcher bodies from servers older than #3842 put the status in `error.code` and the semantic code in @@ -617,6 +651,7 @@ offending field, ready to attach to the inputs that produced them: try { await client.data.create('contact', { email: 'not-an-email' }); } catch (error) { + if (!isApiError(error)) throw error; if (error.code === 'VALIDATION_FAILED') { for (const f of error.fields ?? []) { showFieldError(f.field, f.message); // 'email', 'email must be a valid email address'