Skip to content

Commit 29694a6

Browse files
committed
chore: rules for error discriminator and keys
Signed-off-by: tunnckoCore <5038030+tunnckoCore@users.noreply.github.com>
1 parent 75916cb commit 29694a6

File tree

2 files changed

+27
-18
lines changed

2 files changed

+27
-18
lines changed

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ const uppercaseString = zagora()
122122
uppercased: z.string(),
123123
}))
124124
.errors({
125-
network: z.object({
125+
NETWORK_ERROR: z.object({
126126
type: z.literal("NETWORK_ERROR"),
127127
message: z.string(),
128128
statusCode: z.number().int().min(400).max(599),
129129
retryAfter: z.number().optional(),
130130
}),
131-
validation: z.object({
131+
VALIDATION_ERROR: z.object({
132132
type: z.literal("VALIDATION_ERROR"),
133133
message: z.string(),
134134
field: z.string(),
@@ -137,13 +137,13 @@ const uppercaseString = zagora()
137137
})
138138
.handlerSync((input, err) => {
139139
if (input === "network") {
140-
throw err.network({
140+
throw err.NETWORK_ERROR({
141141
message: "Network failed",
142142
statusCode: 500,
143143
});
144144
}
145145
if (input === "validation") {
146-
throw err.validation({
146+
throw err.VALIDATION_ERROR({
147147
message: "Validation failed",
148148
field: "foo",
149149
value: `some input: ${input}`,
@@ -181,26 +181,27 @@ For example, if you want to have a default values for an error, you should use t
181181
zagora()
182182
.input(z.any())
183183
.errors({
184-
fetchError: z.object({
184+
FETCH_ERR: z.object({
185+
type: z.literal('FETCH_ERR')
185186
message: z.string().default("Unknown error"),
186187
code: z.number().default(500),
187188
}),
188189
})
189190
.handler((_, err) => {
190-
throw err.fetchError({
191+
throw err.FETCH_ERR({
191192
message: 'Custom message',
192193
foo: 123 // type-error, no such property!
193194
});
194195
})
195196
```
196197

197-
If you did `fetchError: z.object().default()`, you would not get type-error if you made a typo mistake when you "called" the error. The following code WILL NOT report a type-error:
198+
If you did `FETCH_ERR: z.object().default()`, you would not get type-error if you made a typo mistake when you "called" the error. The following code WILL NOT report a type-error:
198199

199200
```ts
200201
zagora()
201202
.input(z.any())
202203
.errors({
203-
fetchError: z
204+
FETCH_ERR: z
204205
.object({
205206
message: z.string(),
206207
code: z.number(),
@@ -211,7 +212,7 @@ zagora()
211212
}),
212213
})
213214
.handler((_, err) => {
214-
throw err.fetchError({
215+
throw err.FETCH_ERR({
215216
mssage: 'Custom message', // typo, but not reported
216217
foo: 123 // no such key, but no type-error reported either
217218
});
@@ -246,7 +247,12 @@ console.log({ res });
246247

247248
### Note on error discriminated unions
248249

249-
It's always a good practice to use a consistent naming convention for error types. We use the error's `type` property as discriminator. If you do not provide it in the error schema, you will not be able to discriminate between different error types. The error schema "keys" are used as helper names.
250+
It's always a good practice to use a consistent naming convention for error types. We use the error's `type` property as discriminator. If you do not provide it in the error schema, you will not be able to discriminate between different error types. The `type` property should also match the error key, eg. you will get validation error if it's `someErr: z.object({ type: z.literal("SOME_ERR") })`, because both would mismatch.
251+
252+
**So here are 2 rules of thumb:**
253+
254+
1. Always add the `type` property in the error schema object.
255+
2. Always make sure both the schema key and the `type` property in the schema match.
250256

251257
## Why this over oRPC / tRPC (in some cases)
252258

test/edge-cases.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,25 @@ import z from "zod";
44
import { zagora } from "../src/index.ts";
55

66
test("typed errors handler arg should be second arg when no input schema ", () => {
7+
const errorSchemas = {
8+
SOME_ERR: z.object({
9+
type: z.literal("SOME_ERR"),
10+
msg: z.string().default("Unknown error"),
11+
foo: z.number().min(100).max(999).default(500),
12+
}),
13+
};
14+
715
const func = zagora()
8-
.errors({
9-
someErr: z.object({
10-
type: z.literal("SOME_ERR"),
11-
msg: z.string().default("Unknown error"),
12-
foo: z.number().min(100).max(999).default(500),
13-
}),
14-
})
16+
.errors(errorSchemas)
1517
.handler((_, err) => {
16-
throw err.someErr({ msg: "Custom error" });
18+
throw err.SOME_ERR({ msg: "Custom error" });
1719
});
1820

1921
const res = func();
2022
expect(res.isDefined).toBe(true);
2123

2224
if (res.isDefined && res.error.type === "SOME_ERR") {
25+
expect(Object.keys(errorSchemas)[0]).toBe(res.error.type);
2326
expect(res.error.type).toBe("SOME_ERR");
2427
expect(res.error.msg).toBe("Custom error");
2528
expect(res.error.foo).toBe(500);

0 commit comments

Comments
 (0)