diff --git a/apps/content/docs/error-handling.mdx b/apps/content/docs/error-handling.mdx index 4c8b549a3..2f9e247dd 100644 --- a/apps/content/docs/error-handling.mdx +++ b/apps/content/docs/error-handling.mdx @@ -79,6 +79,10 @@ const exampleProcedure = os }) ``` +:::warning +For compatibility, do not use these error codes: `then`, `bind`, `call`, `apply`, `valueOf`, `toString`, `toJSON`. +::: + :::tip You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance. ::: diff --git a/packages/client/src/consts.ts b/packages/client/src/consts.ts index 144e88199..4dc4665b2 100644 --- a/packages/client/src/consts.ts +++ b/packages/client/src/consts.ts @@ -7,7 +7,7 @@ * proxy for them can cause unexpected behavior, compatibility issues, or * infinite proxy chains. */ -export const RECURSIVE_CLIENT_UNWRAP_KEYS = new Set([ +export const RECURSIVE_CLIENT_UNWRAP_KEYS = new Set([ /** * Prevents the client from being treated as a thenable when users * accidentally write `await client`. diff --git a/packages/contract/src/error-factory.test.ts b/packages/contract/src/error-factory.test.ts index e951dac69..eef5149bd 100644 --- a/packages/contract/src/error-factory.test.ts +++ b/packages/contract/src/error-factory.test.ts @@ -223,17 +223,19 @@ describe('createORPCErrorConstructorMap', () => { expect('ANY_THING' in constructors).toBe(false) }) - it('not proxy on symbol and then keys', async () => { + it('not proxy on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS', async () => { const map = constructors as any expect(map[Symbol('something')]).toBeUndefined() expect(map.then).toBeUndefined() expect(await map).toBe(map) + expect(map.toJSON).toBeUndefined() + expect(String(map)).toBe('[object Object]') }) it('does not resolve error codes through Object.prototype', () => { - const e = (constructors as any).toString() + const e = (constructors as any).constructor() - expect(e.code).toEqual('toString') + expect(e.code).toEqual('constructor') expect(e.defined).toEqual(false) }) }) diff --git a/packages/contract/src/error-factory.ts b/packages/contract/src/error-factory.ts index b0001c872..09c962f17 100644 --- a/packages/contract/src/error-factory.ts +++ b/packages/contract/src/error-factory.ts @@ -3,7 +3,7 @@ import type { MaybeOptionalOptions, Writable } from '@orpc/shared' import type { ErrorMap, ErrorMapItem } from './error' import type { AnySchema, InferSchemaInput, Schema } from './schema' -import { ORPCError } from '@orpc/client' +import { ORPCError, RECURSIVE_CLIENT_UNWRAP_KEYS } from '@orpc/client' import { getOwn, resolveMaybeOptionalOptions } from '@orpc/shared' import { ValidationError } from './error' import { type } from './schema-utils' @@ -156,7 +156,7 @@ export type ORPCErrorConstructorMap = { export function createORPCErrorConstructorMap(errorMap: T): ORPCErrorConstructorMap { const proxy = new Proxy(errorMap, { get(target, code) { - if (typeof code !== 'string' || code === 'then') { + if (typeof code !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(code)) { return Reflect.get(target, code) } diff --git a/packages/pinia-colada/src/router-utils.test.ts b/packages/pinia-colada/src/router-utils.test.ts index 477a58533..94b2487ba 100644 --- a/packages/pinia-colada/src/router-utils.test.ts +++ b/packages/pinia-colada/src/router-utils.test.ts @@ -100,6 +100,11 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(client) as any + expect(String(utils)).toBe('[object Object]') + }) + it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { client.then = vi.fn() diff --git a/packages/pinia-colada/src/router-utils.ts b/packages/pinia-colada/src/router-utils.ts index 830847a75..bb77a846a 100644 --- a/packages/pinia-colada/src/router-utils.ts +++ b/packages/pinia-colada/src/router-utils.ts @@ -125,9 +125,14 @@ function createRouterUtilsInternal( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value } diff --git a/packages/swr/src/router-utils.test.ts b/packages/swr/src/router-utils.test.ts index 564824fdc..3f440d660 100644 --- a/packages/swr/src/router-utils.test.ts +++ b/packages/swr/src/router-utils.test.ts @@ -91,6 +91,11 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(client) as any + expect(String(utils)).toBe('[object Object]') + }) + it('stops recursive on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS of conflicting method proxies', () => { const utils = createRouterUtils(client) as any const keyUtils = utils.key // "key" is both a util method and a nested client diff --git a/packages/swr/src/router-utils.ts b/packages/swr/src/router-utils.ts index d94ecf701..83d4e1bb3 100644 --- a/packages/swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -46,9 +46,14 @@ export function createRouterUtils( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value } diff --git a/packages/tanstack-query/src/router-utils.test.ts b/packages/tanstack-query/src/router-utils.test.ts index 529f65eae..38260cd1d 100644 --- a/packages/tanstack-query/src/router-utils.test.ts +++ b/packages/tanstack-query/src/router-utils.test.ts @@ -94,6 +94,11 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(client) as any + expect(String(utils)).toBe('[object Object]') + }) + it('supports scoped options', () => { const keyOptions = { queryOptions: { diff --git a/packages/tanstack-query/src/router-utils.ts b/packages/tanstack-query/src/router-utils.ts index 36b162cbb..fbeecf169 100644 --- a/packages/tanstack-query/src/router-utils.ts +++ b/packages/tanstack-query/src/router-utils.ts @@ -125,9 +125,14 @@ function createRouterUtilsInternal( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value }