Skip to content
Merged
4 changes: 4 additions & 0 deletions apps/content/docs/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropertyKey>([
/**
* Prevents the client from being treated as a thenable when users
* accidentally write `await client`.
Expand Down
8 changes: 5 additions & 3 deletions packages/contract/src/error-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
4 changes: 2 additions & 2 deletions packages/contract/src/error-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -156,7 +156,7 @@ export type ORPCErrorConstructorMap<T extends ErrorMap> = {
export function createORPCErrorConstructorMap<T extends ErrorMap>(errorMap: T): ORPCErrorConstructorMap<T> {
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)
}

Expand Down
5 changes: 5 additions & 0 deletions packages/pinia-colada/src/router-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
7 changes: 6 additions & 1 deletion packages/pinia-colada/src/router-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,14 @@ function createRouterUtilsInternal<T extends AnyNestedClient>(
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
}

Expand Down
5 changes: 5 additions & 0 deletions packages/swr/src/router-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion packages/swr/src/router-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ export function createRouterUtils<T extends AnyNestedClient>(
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
}

Expand Down
5 changes: 5 additions & 0 deletions packages/tanstack-query/src/router-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
7 changes: 6 additions & 1 deletion packages/tanstack-query/src/router-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,14 @@ function createRouterUtilsInternal<T extends AnyNestedClient>(
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
}

Expand Down
Loading