Skip to content

Commit 58b0d26

Browse files
committed
basic @catch docs
1 parent 0c3a28a commit 58b0d26

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
---
2+
id: catch-directive
3+
title: "@catch Directive"
4+
sidebar_label: "@catch Directive"
5+
---
6+
7+
#### Recommended background reading
8+
9+
- [Relay's @catch directive documentation](https://relay.dev/docs/next/guides/catch-directive/)
10+
- [Error handling in GraphQL](https://spec.graphql.org/draft/#sec-Errors)
11+
12+
## Introduction
13+
14+
The `@catch` directive allows you to handle GraphQL errors explicitly in your application by surfacing them as part of your query/fragment data instead of having them result in `null` values or runtime exceptions. This enables more robust error handling and better user experiences.
15+
16+
## Basic Usage
17+
18+
The `@catch` directive can be applied to:
19+
20+
- Individual fields
21+
- Fragments (at the fragment definition level)
22+
- Operations (queries, mutations, subscriptions)
23+
- Aliased inline fragment spreads
24+
25+
### The `to` Argument
26+
27+
The `@catch` directive accepts an optional `to` argument with two possible values:
28+
29+
- `RESULT` (default): Returns a `RescriptRelay.CatchResult.t<'value>` which is either `Ok({value})` or `Error({errors})`
30+
- `NULL`: If an error occurs, the field value becomes `null` instead of causing the query to fail
31+
32+
## Field-Level @catch
33+
34+
You can catch errors on individual fields:
35+
36+
```rescript
37+
module Query = %relay(`
38+
query UserQuery($id: ID!) {
39+
user(id: $id) {
40+
name @catch
41+
email @catch(to: RESULT)
42+
isOnline @catch(to: NULL)
43+
}
44+
}
45+
`)
46+
47+
@react.component
48+
let make = (~userId) => {
49+
let queryData = Query.use(~variables={id: userId})
50+
51+
switch queryData.user {
52+
| Some(user) =>
53+
<div>
54+
// name field returns CatchResult.t<string>
55+
{switch user.name {
56+
| Ok({value: name}) => <h1>{React.string(name)}</h1>
57+
| Error(_) => <h1>{React.string("Error loading name")}</h1>
58+
}}
59+
60+
// email field returns CatchResult.t<option<string>>
61+
{switch user.email {
62+
| Ok({value: Some(email)}) => <p>{React.string(email)}</p>
63+
| Ok({value: None}) => <p>{React.string("No email")}</p>
64+
| Error(_) => <p>{React.string("Error loading email")}</p>
65+
}}
66+
67+
// isOnline field with to: NULL returns option<bool>
68+
{switch user.isOnline {
69+
| Some(true) => <span>{React.string("Online")}</span>
70+
| Some(false) => <span>{React.string("Offline")}</span>
71+
| None => <span>{React.string("Status unknown")}</span>
72+
}}
73+
</div>
74+
| None => React.null
75+
}
76+
}
77+
```
78+
79+
## Fragment-Level @catch
80+
81+
You can apply `@catch` to entire fragments:
82+
83+
```rescript
84+
module UserFragment = %relay(`
85+
fragment UserProfile_user on User @catch {
86+
name
87+
email
88+
createdAt
89+
}
90+
`)
91+
92+
@react.component
93+
let make = (~userRef) => {
94+
let userData = UserFragment.use(userRef)
95+
96+
switch userData {
97+
| Ok({value: user}) =>
98+
<div>
99+
<h1>{React.string(user.name)}</h1>
100+
<p>{React.string(user.email->Belt.Option.getWithDefault("No email"))}</p>
101+
</div>
102+
| Error(_) =>
103+
<div>{React.string("Error loading user profile")}</div>
104+
}
105+
}
106+
```
107+
108+
## Operation-Level @catch
109+
110+
You can catch errors at the query, mutation, or subscription level:
111+
112+
```rescript
113+
module Query = %relay(`
114+
query UserProfileQuery($id: ID!) @catch {
115+
user(id: $id) {
116+
name
117+
email
118+
posts {
119+
title
120+
content
121+
}
122+
}
123+
}
124+
`)
125+
126+
@react.component
127+
let make = (~userId) => {
128+
let queryData = Query.use(~variables={id: userId})
129+
130+
switch queryData {
131+
| Ok({value: data}) =>
132+
// Handle successful data
133+
switch data.user {
134+
| Some(user) => <UserProfile user />
135+
| None => <div>{React.string("User not found")}</div>
136+
}
137+
| Error(_) =>
138+
<div>{React.string("Error loading user data")}</div>
139+
}
140+
}
141+
```
142+
143+
## Working with `CatchResult.t`
144+
145+
RescriptRelay provides a `CatchResult` module with utilities for working with `@catch` results:
146+
147+
```rescript
148+
module Utils = {
149+
let handleUserName = (nameResult: RescriptRelay.CatchResult.t<string>) => {
150+
switch nameResult {
151+
| Ok({value}) => Some(value)
152+
| Error(_) => None
153+
}
154+
}
155+
156+
// Or use the built-in utility
157+
let handleUserNameWithUtil = (nameResult) => {
158+
nameResult->RescriptRelay.CatchResult.toOption
159+
}
160+
161+
// Convert to a standard Result type
162+
let handleUserNameAsResult = (nameResult) => {
163+
nameResult->RescriptRelay.CatchResult.toResult
164+
}
165+
}
166+
```
167+
168+
## Error Bubbling
169+
170+
When `@catch` is used on a parent field or fragment, errors from child fields will bubble up to the nearest `@catch` directive:
171+
172+
```rescript
173+
module Query = %relay(`
174+
query UserQuery($id: ID!) {
175+
user(id: $id) @catch {
176+
name
177+
email
178+
posts {
179+
title
180+
}
181+
}
182+
}
183+
`)
184+
185+
// If any field within user fails (name, email, or posts.title),
186+
// the error will be caught at the user level
187+
```
188+
189+
## Nested @catch Directives
190+
191+
You can have multiple levels of `@catch` directives. Errors are caught by the closest `@catch` ancestor:
192+
193+
```rescript
194+
module Query = %relay(`
195+
query UserQuery($id: ID!) @catch {
196+
user(id: $id) {
197+
name @catch
198+
posts @catch {
199+
title
200+
author {
201+
name
202+
}
203+
}
204+
}
205+
}
206+
`)
207+
208+
@react.component
209+
let make = (~userId) => {
210+
let queryData = Query.use(~variables={id: userId})
211+
212+
switch queryData {
213+
| Ok({value: data}) =>
214+
switch data.user {
215+
| Some(user) =>
216+
<div>
217+
// name errors are caught at field level
218+
{switch user.name {
219+
| Ok({value: name}) => <h1>{React.string(name)}</h1>
220+
| Error(_) => <h1>{React.string("Name unavailable")}</h1>
221+
}}
222+
223+
// posts errors are caught at posts field level
224+
{switch user.posts {
225+
| Ok({value: posts}) =>
226+
posts->Array.map(post =>
227+
<div key={post.title}>{React.string(post.title)}</div>
228+
)->React.array
229+
| Error(_) => <div>{React.string("Posts unavailable")}</div>
230+
}}
231+
</div>
232+
| None => React.null
233+
}
234+
| Error(_) => <div>{React.string("Error loading data")}</div>
235+
}
236+
}
237+
```
238+
239+
## Union and Interface Handling
240+
241+
`@catch` works seamlessly with unions and interfaces:
242+
243+
```rescript
244+
module MemberFragment = %relay(`
245+
fragment MemberProfile_member on Member @catch {
246+
... on User {
247+
id
248+
name
249+
email
250+
}
251+
... on Group {
252+
id
253+
name
254+
memberCount
255+
}
256+
}
257+
`)
258+
259+
@react.component
260+
let make = (~memberRef) => {
261+
let memberData = MemberFragment.use(memberRef)
262+
263+
switch memberData {
264+
| Ok({value: member}) =>
265+
switch member {
266+
| User(user) => <UserCard user />
267+
| Group(group) => <GroupCard group />
268+
}
269+
| Error(_) => <div>{React.string("Error loading member")}</div>
270+
}
271+
}
272+
```
273+
274+
## Mutations with @catch
275+
276+
Handle mutation errors gracefully:
277+
278+
```rescript
279+
module UpdateUserMutation = %relay(`
280+
mutation UpdateUserMutation($input: UpdateUserInput!) @catch {
281+
updateUser(input: $input) {
282+
user {
283+
id
284+
name
285+
email
286+
}
287+
errors {
288+
field
289+
message
290+
}
291+
}
292+
}
293+
`)
294+
295+
@react.component
296+
let make = () => {
297+
let (mutate, isMutating) = UpdateUserMutation.use()
298+
299+
let handleUpdate = () => {
300+
mutate(
301+
~variables={input: {name: "New Name"}},
302+
~onCompleted=(result, _errors) => {
303+
switch result {
304+
| Ok({value: data}) =>
305+
switch data.updateUser {
306+
| Some({user: Some(user), errors: []}) =>
307+
Console.log("User updated successfully")
308+
| Some({errors}) =>
309+
Console.log2("Validation errors:", errors)
310+
| _ => Console.log("Unexpected response")
311+
}
312+
| Error({errors}) =>
313+
Console.log2("Network/GraphQL errors:", errors)
314+
}
315+
}
316+
)
317+
}
318+
319+
<button onClick={_ => handleUpdate()}>
320+
{React.string(isMutating ? "Updating..." : "Update User")}
321+
</button>
322+
}
323+
```
324+
325+
## Type Safety and Nullability
326+
327+
Fields with `@catch` are typed according to their semantic nullability rather than their schema nullability. This means:
328+
329+
- Non-null fields in the schema that are marked as semantically nullable will be typed as non-null in ReScript when caught
330+
- The error handling is explicit through the `CatchResult` type rather than through nullable types
331+
332+
## Interaction with @throwOnFieldError
333+
334+
The `@catch` directive works in conjunction with `@throwOnFieldError`. Key points:
335+
336+
- **Without @throwOnFieldError**: Field errors result in `null` values by default. Using `@catch` surfaces these errors in the data object instead
337+
- **With @throwOnFieldError**: You can use `@catch` on specific fields to handle those errors locally instead of throwing exceptions
338+
- **@catch takes precedence**: Fields with `@catch` will not throw exceptions, even within a `@throwOnFieldError` context
339+
340+
For detailed information about `@throwOnFieldError`, see the [official Relay documentation](https://relay.dev/docs/next/guides/throw-on-field-error-directive/).
341+
342+
## Limitations
343+
344+
- `@catch` cannot be used with `@required` on the same field
345+
- `@catch` cannot be used on unaliased inline fragments
346+
- `@catch` cannot be used on fragment spreads without alias

rescript-relay-documentation/docs/making-queries.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ The results are delivered through a `RescriptCore.Result.t` in the `onResult` ca
322322

323323
> `fetch` uses Relay's `fetchQuery` under the hood, which you can [read more about here](https://relay.dev/docs/api-reference/fetch-query).
324324
325+
> For more robust error handling within your GraphQL operations, consider using the [`@catch` directive](catch-directive) to handle field-level errors explicitly in your query data.
326+
325327
##### Parameters
326328

327329
| Name | Type | Required | Notes |

rescript-relay-documentation/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module.exports = {
4141
"interacting-with-the-store",
4242
"client-schema-extensions",
4343
"relay-resolvers",
44+
"catch-directive",
4445
"codesplit-directive",
4546
],
4647
"API Reference": ["api-reference", "relay-environment"],

0 commit comments

Comments
 (0)