Skip to content

Commit a6366d9

Browse files
authored
feat!: remove typesafe returned errors and the inferable flag (#1987)
Handlers can no longer signal an error by returning an `ORPCError`. What a handler returns is its output, what it throws is an error, and `.errors` or the `error()` factory is the only way to get a typesafe error. With returned errors gone the `inferable` flag always equalled `defined`, so it is removed and `isInferableError` goes back to its v1 name `isDefinedError`. Implements the proposal in #1982. ## Breaking changes - A returned `ORPCError` is plain output. Under an output schema it is a type error. - `Procedure`, `DecoratedProcedure`, `ProcedureClient`, `ProcedureClientOptions`, `CallOptions` and the Next.js server function types lose their trailing `TReturnedError` generic. `opaqueReturnedErrors` is gone from the procedure definition. - `ORPCError` and `ORPCErrorJSON` no longer carry `inferable`. The error response body and generated OpenAPI error schemas drop the field, so clients on this build reject error bodies from earlier v2 betas that still send it. - `isInferableError` is renamed to `isDefinedError` with no alias, and `safe` exposes `definedError` instead of `inferableError`. - Effect: an `ORPCError` that fails the effect is thrown as-is instead of becoming an inferable error. ## Performance - No `instanceof` check or error clone on the handler output path. - One less generic on every procedure and client type, and `.handler` no longer splits its return type with `Exclude`/`Extract`. ## Docs - Removed the "Returning an ORPCError" section, rewrote the Effect typesafe-errors section around `.errors`, renamed the client error-handling anchor to `#using-safe-and-isdefinederror`, and updated the v1 migration guide, RPC protocol example, Next.js docs, skills and playgrounds. ## Testing - Root and per-package type checks, eslint and the JSDoc backlink checker pass. - Full vitest passes. Type tests now assert that a returned `ORPCError` is output and is rejected under an output schema.
1 parent e99ad16 commit a6366d9

101 files changed

Lines changed: 481 additions & 1226 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/content/docs/client/client-side.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ const output = await client.someProcedure(input, {
8383
Interceptors let you wrap client calls. They are similar to interceptors in links, but are more typesafe because the exact input, output, and error types of each client are known. You can provide per-client interceptors with `scoped`.
8484

8585
```ts
86-
import { isInferableError, safe } from '@orpc/client'
86+
import { isDefinedError, safe } from '@orpc/client'
8787

8888
const client: RouterClient<typeof router, ClientContext> = createORPCClient(link, {
8989
interceptors: [
9090
async ({ context, path, next }) => {
9191
const [error, data] = await safe(next())
9292

9393
if (error) {
94-
if (isInferableError(error)) {
94+
if (isDefinedError(error)) {
9595
// handle typesafe errors
9696
}
9797

@@ -116,7 +116,7 @@ const client: RouterClient<typeof router, ClientContext> = createORPCClient(link
116116
```
117117

118118
:::info
119-
You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors.
119+
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
120120
:::
121121

122122
## Merging Clients

apps/content/docs/client/error-handling.mdx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ catch (error) {
1818
}
1919
```
2020

21-
## Using `safe` and `isInferableError`
21+
## Using `safe` and `isDefinedError`
2222

2323
When working with [Typesafe Errors](/docs/error-handling#typesafe-errors), use `safe` to preserve error type inference. It behaves like `try/catch`, but returns the typesafe result instead of throwing.
2424

2525
```ts twoslash
2626
import { call, os } from '@orpc/server'
2727
import * as z from 'zod'
2828
// ---cut---
29-
import { isInferableError, safe } from '@orpc/client'
29+
import { isDefinedError, safe } from '@orpc/client'
3030

3131
const exampleProcedure = os
3232
.input(z.object({ id: z.string() }))
@@ -39,15 +39,15 @@ const exampleProcedure = os
3939
throw errors.RATE_LIMIT_EXCEEDED({ data: { retryAfter: 1000 } })
4040
})
4141

42-
// or { error, data, inferableError }
43-
const [error, data, inferableError] = await safe(
42+
// or { error, data, definedError }
43+
const [error, data, definedError] = await safe(
4444
call(exampleProcedure, { id: '123' })
4545
)
4646

47-
if (isInferableError(error)) { // or inferableError
48-
// handle inferable error
47+
if (isDefinedError(error)) { // or definedError
48+
// handle defined error
4949

50-
// or inferableError.data.retryAfter
50+
// or definedError.data.retryAfter
5151
console.log(error.data.retryAfter)
5252
}
5353
else if (error) {
@@ -62,10 +62,10 @@ else {
6262
:::info
6363
`safe` supports both tuple and object forms:
6464

65-
- `[error, data, inferableError]`
66-
- `{ error, data, inferableError }`
65+
- `[error, data, definedError]`
66+
- `{ error, data, definedError }`
6767

68-
`inferableError` is the same value as `error` when `isInferableError(error)` returns `true`; otherwise it is `null`.
68+
`definedError` is the same value as `error` when `isDefinedError(error)` returns `true`; otherwise it is `null`.
6969
:::
7070

7171
## Safe Client

apps/content/docs/error-handling.mdx

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const example = os
3636

3737
## Typesafe Errors
3838

39-
For end-to-end type safety, define your errors with `.errors` or [return `ORPCError`](#returning-an-orpcerror). This lets the client infer each error's shape and handle it safely. You can use any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate error data.
39+
For end-to-end type safety, define your errors with `.errors`. This lets the client infer each error's shape and handle it safely. You can use any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate error data.
4040

4141
:::danger
4242
`message` and `data` are sent to the client. Do not include sensitive information in either field.
@@ -105,32 +105,6 @@ const exampleProcedure = os
105105
})
106106
```
107107

108-
### Returning an `ORPCError`
109-
110-
As an alternative to `.errors`, you can return an `ORPCError` directly from your handler or middleware to achieve end-to-end type safety.
111-
112-
:::warning
113-
When [implementing a contract](/docs/contract/implementation), returning an `ORPCError` is equivalent to throwing one.
114-
:::
115-
116-
```ts
117-
const exampleProcedure = os
118-
.handler(async ({ errors }) => {
119-
if (reachRateLimit) {
120-
return new ORPCError('RATE_LIMITED', {
121-
message: 'You are being rate limited',
122-
data: { retryAfter: 60 }
123-
})
124-
}
125-
126-
return 'Success'
127-
})
128-
```
129-
130-
:::danger
131-
`message` and `data` are sent to the client. Do not include sensitive information in either field.
132-
:::
133-
134108
## Error Factory
135109

136110
An error factory lets you define an error once and reuse it anywhere, keeping error handling consistent across your project.

apps/content/docs/integrations/effect.mdx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -153,26 +153,26 @@ For app level error handling, we recommend [middleware](/docs/middleware) or int
153153

154154
### Typesafe Errors
155155

156-
When you `yield* Effect.fail(new ORPCError(...))` or `return new ORPCError(...)`, oRPC treats it as a [returned ORPCError](/docs/error-handling#returning-an-orpcerror). On the client, you can handle these errors in a typesafe way:
156+
An `ORPCError` that fails the effect, such as `yield* Effect.fail(new ORPCError(...))`, is thrown from the handler exactly like a thrown `ORPCError` in a regular handler. Define your errors with `.errors` and fail with `errors.X(...)` to make them [typesafe](/docs/error-handling#typesafe-errors) on the client:
157157

158158
```ts
159-
const procedure = os.handler(handlerGen(function* ({ errors }) {
160-
if (resourceNotFound) {
161-
yield* Effect.fail(new ORPCError('NOT_FOUND', {
162-
message: 'The resource you are looking for does not exist',
163-
}))
164-
// -- or -
165-
return new ORPCError('NOT_FOUND', {
159+
const procedure = os
160+
.errors({
161+
NOT_FOUND: {
166162
message: 'The resource you are looking for does not exist',
167-
})
168-
}
163+
},
164+
})
165+
.handler(handlerGen(function* ({ errors }) {
166+
if (resourceNotFound) {
167+
yield* Effect.fail(errors.NOT_FOUND())
168+
}
169169

170-
return 'Success'
171-
}))
170+
return 'Success'
171+
}))
172172

173-
const [error, result] = await call(procedure)
173+
const [error, result] = await safe(call(procedure))
174174

175-
if (isInferableError(error)) {
175+
if (isDefinedError(error)) {
176176
// typesafe error handling
177177
}
178178
```
@@ -244,14 +244,14 @@ const program = Effect.gen(function* () {
244244
)
245245
```
246246

247-
You can also combine `Effect.catchIf` with [isInferableError](/docs/client/error-handling#using-safe-and-isinferableerror) to recover from every inferable error in a typesafe way:
247+
You can also combine `Effect.catchIf` with [isDefinedError](/docs/client/error-handling#using-safe-and-isdefinederror) to recover from every defined error in a typesafe way:
248248

249249
```ts
250-
import { isInferableError } from '@orpc/client'
250+
import { isDefinedError } from '@orpc/client'
251251
import { Effect } from 'effect'
252252

253253
const recovered = effectClient.planet.find({ id: 1 }).pipe(
254-
Effect.catchIf(isInferableError, (error) => {
254+
Effect.catchIf(isDefinedError, (error) => {
255255
// error is fully typed here
256256
return Effect.succeed(null)
257257
}),

apps/content/docs/integrations/next.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Special Next.js errors such as [redirect](https://nextjs.org/docs/app/api-refere
5959

6060
### Typesafe Errors
6161

62-
[Typesafe errors](/docs/error-handling#typesafe-errors) are supported as well. Because errors are serialized before they reach the client, use the `inferable` field to distinguish errors.
62+
[Typesafe errors](/docs/error-handling#typesafe-errors) are supported as well. Because errors are serialized before they reach the client, use the `defined` field to distinguish errors.
6363

6464
<CodeGroup>
6565

@@ -73,7 +73,7 @@ export default function Page() {
7373
const [error, message] = await serverFunction()
7474

7575
if (error) {
76-
if (error.inferable) {
76+
if (error.defined) {
7777
// handle typesafe error
7878
}
7979
else {
@@ -165,7 +165,7 @@ This integration also includes React hooks for server functions. `useServerFunct
165165
```tsx useServerFunction
166166
'use client'
167167

168-
import { isInferableError } from '@orpc/client'
168+
import { isDefinedError } from '@orpc/client'
169169
import {
170170
getIssueMessage,
171171
onErrorDeferred,
@@ -177,7 +177,7 @@ export function MyComponent() {
177177
const { execute, data, error, status } = useServerFunction(serverFunction, {
178178
interceptors: [
179179
onErrorDeferred((error) => {
180-
if (isInferableError(error)) {
180+
if (isDefinedError(error)) {
181181
console.error(error.data)
182182
// ^ Typed error data
183183
}
@@ -244,7 +244,7 @@ Besides hooks, this integration also re-exports [form-data helpers](/docs/helper
244244
:::
245245

246246
:::info
247-
You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors.
247+
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
248248
:::
249249

250250
## Server Form Functions

apps/content/docs/integrations/pinia-colada.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ When you configure `queryKey`, it also affects `.queryOptions` because it is use
232232
Interceptors let you wrap `query` and `mutation` calls. Unlike [default options](#default-options), which can be overridden by per-call options, interceptors always run for every query and mutation.
233233

234234
```ts
235-
import { isInferableError, safe } from '@orpc/client'
235+
import { isDefinedError, safe } from '@orpc/client'
236236

237237
const orpc = createPiniaColadaUtils(client, {
238238
queryInterceptors: [],
@@ -244,7 +244,7 @@ const orpc = createPiniaColadaUtils(client, {
244244
const [error, data] = await safe(next())
245245

246246
if (error) {
247-
if (isInferableError(error)) {
247+
if (isDefinedError(error)) {
248248
// handle typesafe errors
249249
}
250250

@@ -258,7 +258,7 @@ const orpc = createPiniaColadaUtils(client, {
258258
```
259259

260260
:::info
261-
You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors.
261+
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
262262
:::
263263

264264
## Plugins
@@ -401,22 +401,22 @@ const link = new RPCLink<ClientContext>({
401401

402402
## Typesafe Error Handling
403403

404-
Use the built-in `isInferableError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations.
404+
Use the built-in `isDefinedError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations.
405405

406406
```ts
407-
import { isInferableError } from '@orpc/client'
407+
import { isDefinedError } from '@orpc/client'
408408

409409
const mutation = useMutation(orpc.planet.create.mutationOptions({
410410
onError: (error) => {
411-
if (isInferableError(error)) {
411+
if (isDefinedError(error)) {
412412
// Handle typesafe errors here
413413
}
414414
}
415415
}))
416416

417417
mutation.mutate({ name: 'Earth' })
418418

419-
if (mutation.error.value && isInferableError(mutation.error.value)) {
419+
if (mutation.error.value && isDefinedError(mutation.error.value)) {
420420
// Handle the typesafe errors here
421421
}
422422
```

apps/content/docs/integrations/tanstack-query.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ When you configure `queryKey`, it also affects `.queryOptions` because it is use
266266
Interceptors let you wrap `queryFn` and `mutationFn` calls. Unlike [default options](#default-options), which can be overridden by per-call options, interceptors always run for every query and mutation.
267267

268268
```ts
269-
import { isInferableError, safe } from '@orpc/client'
269+
import { isDefinedError, safe } from '@orpc/client'
270270

271271
const orpc = createTanstackQueryUtils(client, {
272272
queryInterceptors: [],
@@ -278,7 +278,7 @@ const orpc = createTanstackQueryUtils(client, {
278278
const [error, data] = await safe(next())
279279

280280
if (error) {
281-
if (isInferableError(error)) {
281+
if (isDefinedError(error)) {
282282
// handle typesafe errors
283283
}
284284

@@ -305,7 +305,7 @@ const orpc = createTanstackQueryUtils(client, {
305305
```
306306

307307
:::info
308-
You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors.
308+
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
309309
:::
310310

311311
## Plugins
@@ -462,22 +462,22 @@ const link = new RPCLink<ClientContext>({
462462

463463
## Typesafe Error Handling
464464

465-
Use the built-in `isInferableError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations.
465+
Use the built-in `isDefinedError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations.
466466

467467
```ts
468-
import { isInferableError } from '@orpc/client'
468+
import { isDefinedError } from '@orpc/client'
469469

470470
const mutation = useMutation(orpc.planet.create.mutationOptions({
471471
onError: (error) => {
472-
if (isInferableError(error)) {
472+
if (isDefinedError(error)) {
473473
// Handle typesafe errors here
474474
}
475475
}
476476
}))
477477

478478
mutation.mutate({ name: 'Earth' })
479479

480-
if (mutation.error && isInferableError(mutation.error)) {
480+
if (mutation.error && isDefinedError(mutation.error)) {
481481
// Handle the typesafe errors here
482482
}
483483
```

apps/content/docs/migrations/from-v1.mdx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ The Hey API and Durable Iterator integrations no longer exist in v2. In place of
4949
Two formats changed on the wire:
5050

5151
- The [RPC serializer](/docs/rpc/serializer) format, described in the [RPC Protocol](/docs/rpc/protocol).
52-
- The error response body, which adds an `inferable` field and no longer contains a `status` field, since [`status` was removed from errors](#status-removed-from-errors).
52+
- The error response body, which no longer contains a `status` field, since [`status` was removed from errors](#status-removed-from-errors).
5353

5454
Because of these changes, a v1 [RPC Link](/docs/rpc/link) or [OpenAPI Link](/docs/openapi/link) cannot talk to a v2 server (and vice versa). Deploy the upgraded server and clients together.
5555

@@ -443,19 +443,19 @@ const handler = new RPCHandler(router)
443443

444444
</CodeGroup>
445445

446-
### `isDefinedError` renamed to `isInferableError`
446+
### `safe` result changed
447447

448-
`isDefinedError` still works as a deprecated alias. The `safe` result also changed: the third element is now the typed error itself (or `null`) instead of a boolean, and a fourth `isSuccess` element was added. See [Client Error Handling](/docs/client/error-handling).
448+
The third element of the `safe` result is now the typed error itself (or `null`) instead of a boolean, and a fourth `isSuccess` element was added. See [Client Error Handling](/docs/client/error-handling).
449449

450450
<CodeGroup>
451451

452452
```ts v2
453-
import { isInferableError, safe } from '@orpc/client'
453+
import { isDefinedError, safe } from '@orpc/client'
454454

455-
const [error, data, inferableError, isSuccess] = await safe(client.example({ id: 1 }))
455+
const [error, data, definedError, isSuccess] = await safe(client.example({ id: 1 }))
456456

457-
if (inferableError) {
458-
console.log(inferableError.data.retryAfter)
457+
if (definedError) {
458+
console.log(definedError.data.retryAfter)
459459
}
460460
```
461461

@@ -1269,7 +1269,6 @@ These renames still compile through deprecated aliases, so you can migrate them
12691269

12701270
| v1 name | v2 name | Package |
12711271
| ---------------------------- | ----------------------------- | -------------------------------- |
1272-
| `isDefinedError` | `isInferableError` | `@orpc/client` |
12731272
| `InferClientErrorUnion` | `InferClientError` | `@orpc/client` |
12741273
| `ClientPromiseResult` | `PromiseWithError` | `@orpc/client` |
12751274
| `eventIterator` | `asyncIteratorObject` | `@orpc/server`, `@orpc/contract` |

apps/content/docs/recipes/no-throw-literal.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ declare module '@orpc/server' { // or '@orpc/contract', or '@orpc/client'
2626
```
2727

2828
:::info
29-
Avoid using `any` or `unknown` for `ThrowableError` because doing so prevents the client from inferring [typesafe errors](/docs/client/error-handling#using-safe-and-isinferableerror). Instead, use `null | undefined | {}` (equivalent to `unknown`) for stricter error type inference.
29+
Avoid using `any` or `unknown` for `ThrowableError` because doing so prevents the client from inferring [typesafe errors](/docs/client/error-handling#using-safe-and-isdefinederror). Instead, use `null | undefined | {}` (equivalent to `unknown`) for stricter error type inference.
3030
:::
3131

3232
:::warning
@@ -36,7 +36,7 @@ If `ThrowableError` is configured as `null | undefined | {}`, check `isSuccess`
3636
const { error, data, isSuccess } = await safe(client('input'))
3737

3838
if (!isSuccess) {
39-
if (isInferableError(error)) {
39+
if (isDefinedError(error)) {
4040
// handle typesafe errors
4141
}
4242

0 commit comments

Comments
 (0)