Skip to content

Commit 4dd5338

Browse files
committed
feat(qwik-router): isServerError() guard for narrowing errors in catch blocks
1 parent 8e662a6 commit 4dd5338

11 files changed

Lines changed: 160 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/router': minor
3+
---
4+
5+
feat: new `isServerError()` guard for narrowing errors in `catch` blocks (around `submit()` or `resolveValue()`), since `instanceof ServerError` doesn't survive client-side deserialization.

packages/docs/src/routes/api/qwik-router-middleware-request-handler/api.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,20 @@
356356
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts",
357357
"mdFile": "router.internalrequest.md"
358358
},
359+
{
360+
"name": "isServerError",
361+
"id": "isservererror",
362+
"hierarchy": [
363+
{
364+
"name": "isServerError",
365+
"id": "isservererror"
366+
}
367+
],
368+
"kind": "Function",
369+
"content": "Checks whether an error is a `ServerError` (carries `status` and `data`<!-- -->). Structural rather than `instanceof` so it also matches ServerErrors that crossed a serialization boundary.\n\n\n```typescript\nexport declare function isServerError<E>(err: ServerError<E> | Error | undefined): err is ServerError<E>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nerr\n\n\n</td><td>\n\n[ServerError](#servererror-variable)<!-- -->&lt;E&gt; \\| Error \\| undefined\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nerr is [ServerError](#servererror-variable)<!-- -->&lt;E&gt;",
370+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/fail.ts",
371+
"mdFile": "router.isservererror.md"
372+
},
359373
{
360374
"name": "mergeHeadersCookies",
361375
"id": "mergeheaderscookies",

packages/docs/src/routes/api/qwik-router-middleware-request-handler/index.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,48 @@ export type InternalRequest = false | "loader" | "action";
872872
873873
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts)
874874
875+
<h2 id="isservererror">isServerError</h2>
876+
877+
Checks whether an error is a `ServerError` (carries `status` and `data`). Structural rather than `instanceof` so it also matches ServerErrors that crossed a serialization boundary.
878+
879+
```typescript
880+
export declare function isServerError<E>(
881+
err: ServerError<E> | Error | undefined,
882+
): err is ServerError<E>;
883+
```
884+
885+
<table><thead><tr><th>
886+
887+
Parameter
888+
889+
</th><th>
890+
891+
Type
892+
893+
</th><th>
894+
895+
Description
896+
897+
</th></tr></thead>
898+
<tbody><tr><td>
899+
900+
err
901+
902+
</td><td>
903+
904+
[ServerError](#servererror-variable)&lt;E&gt; \| Error \| undefined
905+
906+
</td><td>
907+
908+
</td></tr>
909+
</tbody></table>
910+
911+
**Returns:**
912+
913+
err is [ServerError](#servererror-variable)&lt;E&gt;
914+
915+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/fail.ts)
916+
875917
<h2 id="mergeheaderscookies">mergeHeadersCookies</h2>
876918
877919
```typescript

packages/docs/src/routes/api/qwik-router/api.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,20 @@
408408
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
409409
"mdFile": "router.httperrorprops.md"
410410
},
411+
{
412+
"name": "isServerError",
413+
"id": "isservererror",
414+
"hierarchy": [
415+
{
416+
"name": "isServerError",
417+
"id": "isservererror"
418+
}
419+
],
420+
"kind": "Function",
421+
"content": "Checks whether an error is a `ServerError` (carries `status` and `data`<!-- -->). Structural rather than `instanceof` so it also matches ServerErrors that crossed a serialization boundary.\n\n\n```typescript\nexport declare function isServerError<E>(err: ServerError<E> | Error | undefined): err is ServerError<E>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nerr\n\n\n</td><td>\n\n[ServerError](#servererror-variable)<!-- -->&lt;E&gt; \\| Error \\| undefined\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nerr is [ServerError](#servererror-variable)<!-- -->&lt;E&gt;",
422+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/fail.ts",
423+
"mdFile": "router.isservererror.md"
424+
},
411425
{
412426
"name": "JSONObject",
413427
"id": "jsonobject",

packages/docs/src/routes/api/qwik-router/index.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,48 @@ export type HttpStatus = {
11301130

11311131
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts)
11321132

1133+
<h2 id="isservererror">isServerError</h2>
1134+
1135+
Checks whether an error is a `ServerError` (carries `status` and `data`). Structural rather than `instanceof` so it also matches ServerErrors that crossed a serialization boundary.
1136+
1137+
```typescript
1138+
export declare function isServerError<E>(
1139+
err: ServerError<E> | Error | undefined,
1140+
): err is ServerError<E>;
1141+
```
1142+
1143+
<table><thead><tr><th>
1144+
1145+
Parameter
1146+
1147+
</th><th>
1148+
1149+
Type
1150+
1151+
</th><th>
1152+
1153+
Description
1154+
1155+
</th></tr></thead>
1156+
<tbody><tr><td>
1157+
1158+
err
1159+
1160+
</td><td>
1161+
1162+
[ServerError](#servererror-variable)&lt;E&gt; \| Error \| undefined
1163+
1164+
</td><td>
1165+
1166+
</td></tr>
1167+
</tbody></table>
1168+
1169+
**Returns:**
1170+
1171+
err is [ServerError](#servererror-variable)&lt;E&gt;
1172+
1173+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/fail.ts)
1174+
11331175
<h2 id="jsonobject">JSONObject</h2>
11341176

11351177
```typescript

packages/qwik-router/src/middleware/request-handler/fail.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ export function getFailMeta(value: Failed): FailMeta {
6565
return value[FailBrand];
6666
}
6767

68+
/**
69+
* Checks whether an error is a `ServerError` (carries `status` and `data`). Structural rather than
70+
* `instanceof` so it also matches ServerErrors that crossed a serialization boundary.
71+
*
72+
* @public
73+
*/
74+
export function isServerError<E>(err: ServerError<E> | Error | undefined): err is ServerError<E>;
75+
/** @public */
76+
export function isServerError<T = unknown>(err: unknown): err is ServerError<T>;
77+
export function isServerError(err: unknown): boolean {
78+
return (
79+
err instanceof Error &&
80+
typeof (err as { status?: unknown }).status === 'number' &&
81+
'data' in err
82+
);
83+
}
84+
6885
export function failToServerError(value: FailReturn<Record<string, any>>): ServerError {
6986
const { ...payload } = value;
7087
return new ServerError(getFailMeta(value).status, payload);

packages/qwik-router/src/middleware/request-handler/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export { isStaticPath } from './static-paths';
88
export { mergeHeadersCookies } from './cookie';
99

1010
export { ServerError } from './server-error';
11-
export { FailBrand } from './fail';
11+
export { FailBrand, isServerError } from './fail';
1212
export type { Failed, FailMeta, FailReturn, ExcludeFail, FailPayload } from './fail';
1313
export { AbortMessage, RedirectMessage } from './redirect-handler';
1414
export { RewriteMessage } from './rewrite-handler';

packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ export function getNotFound(prefix: string): string;
123123
// @public
124124
export type InternalRequest = false | 'loader' | 'action';
125125

126+
// @public
127+
export function isServerError<E>(err: ServerError<E> | Error | undefined): err is ServerError<E>;
128+
129+
// @public (undocumented)
130+
export function isServerError<T = unknown>(err: unknown): err is ServerError<T>;
131+
126132
// Warning: (ae-internal-missing-underscore) The name "isStaticPath" should be prefixed with an underscore because the declaration is marked as @internal
127133
//
128134
// @internal

packages/qwik-router/src/middleware/request-handler/server-error.unit.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, expectTypeOf, test } from 'vitest';
2+
import { isServerError } from './fail';
23
import { ServerError } from './server-error';
34

45
describe('ServerError payload flattening', () => {
@@ -56,3 +57,14 @@ describe('ServerError payload flattening', () => {
5657
expectTypeOf(err.data.message).toEqualTypeOf<string[]>();
5758
});
5859
});
60+
61+
describe('isServerError', () => {
62+
test('matches real instances and serialization-shaped plain Errors', () => {
63+
expect(isServerError(new ServerError(404, { reason: 'x' }))).toBe(true);
64+
const wireShaped = Object.assign(new Error('x'), { status: 404, data: { reason: 'x' } });
65+
expect(isServerError(wireShaped)).toBe(true);
66+
expect(isServerError(new Error('network'))).toBe(false);
67+
expect(isServerError(undefined)).toBe(false);
68+
expect(isServerError({ status: 404, data: {} })).toBe(false);
69+
});
70+
});

packages/qwik-router/src/runtime/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
zodQrl,
9191
} from './server-functions';
9292
export { routeLoader$, routeLoaderQrl } from './route-loaders';
93+
export { isServerError } from '../../middleware/request-handler/fail';
9394
export { ServerError } from '../../middleware/request-handler/server-error';
9495
export { ServiceWorkerRegister } from './sw-component';
9596
export {

0 commit comments

Comments
 (0)