Commit 2527975
feat(client): safe client (#751)
This PR implements the `createSafeClient` feature requested in #703,
which provides automatic safe error handling for oRPC client calls
without requiring manual wrapping.
## Problem
Previously, users had to manually wrap each client call with the `safe`
function:
```typescript
const { error, data, isDefined } = await safe(client.doSomething({ id: '123' }))
```
This became repetitive when you wanted all calls to use safe error
handling.
## Solution
Added `createSafeClient` function that wraps an entire client to
automatically apply safe error handling:
```typescript
const safeClient = createSafeClient(client)
const { error, data, isDefined } = await safeClient.doSomething({ id: '123' })
```
## Implementation Details
- **Proxy-based interception**: Uses JavaScript Proxy to intercept both
property access (for nested clients) and function calls (for procedure
execution)
- **Type safety**: Added `SafeClient<T>` type that transforms client
methods to return `Promise<SafeResult<...>>` instead of
`ClientPromiseResult<...>`
- **Full compatibility**: Supports all existing client features
including nested procedures, client options (signal, context), and both
object/tuple destructuring
- **Zero breaking changes**: Purely additive feature that doesn't modify
existing APIs
## Features
✅ **Automatic error handling** - All procedure calls return safe results
✅ **Nested procedure support** - Works with
`safeClient.user.profile.get()`
✅ **Client options** - Supports signals, context, and other options
✅ **Type safety** - Full TypeScript support with proper inference
✅ **Destructuring** - Both `{ error, data }` and `[error, data]` styles
## Examples
### Basic Usage
```typescript
import { createSafeClient } from '@orpc/client'
const safeClient = createSafeClient(client)
// Object destructuring
const { error, data, isDefined, isSuccess } = await safeClient.getUser({ id: '123' })
// Tuple destructuring
const [error, data, isDefined, isSuccess] = await safeClient.getUser({ id: '123' })
```
### Error Handling
```typescript
const { error, data, isDefined } = await safeClient.getUser({ id: 'invalid' })
if (error) {
if (isDefined) {
// Defined ORPC error with structured data
console.log('Error code:', error.code)
} else {
// Regular error
console.log('Error:', error.message)
}
} else {
console.log('Success:', data)
}
```
### Nested Procedures
```typescript
// All levels automatically wrapped
const result = await safeClient.admin.users.list({ page: 1 })
```
## Testing
- Added 5 comprehensive unit tests covering success/error cases, nested
calls, and client options
- Added 4 integration tests demonstrating real-world usage patterns
- Added TypeScript type tests to ensure proper type inference
- All 534 existing tests continue to pass
- Verified build, linting, and type checking
Fixes #703.
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: unnoq <64189902+unnoq@users.noreply.github.com>
Co-authored-by: unnoq <contact@unnoq.com>1 parent 414c2c9 commit 2527975
File tree
5 files changed
+140
-3
lines changed- apps/content/docs/client
- packages/client/src
5 files changed
+140
-3
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
0 commit comments