Skip to content

Commit 5282a39

Browse files
authored
fix: standardise error handling (#7185)
* fix: pass server$ errors through to middleware * fix e2e tests * Add changeset * Add middlewaare error test and remove ErrorResponse * Update changeset * Add error handling docs * Allow returning plain text ServerErrors * Improvements * Update docs * test server errors with text/html accept headers * Update changesets * Update ErrorResponse and vite bug url
1 parent 68b2bd9 commit 5282a39

File tree

23 files changed

+369
-58
lines changed

23 files changed

+369
-58
lines changed

.changeset/furry-paint-spin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik-city': minor
3+
---
4+
5+
fix: server$ errors can be caught by @plugin middleware

.changeset/loud-fish-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik-city': minor
3+
---
4+
5+
refactor: Error types are standardised across server$ functions and routeLoaders

.changeset/quiet-squids-run.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik-city': minor
3+
---
4+
5+
feat: 499 is now a valid status code

.changeset/tall-flowers-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik-city': minor
3+
---
4+
5+
fix: server$ functions now correctly throw 4xx errors on the client

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@
327327
}
328328
],
329329
"kind": "Interface",
330-
"content": "```typescript\nexport interface RequestEventCommon<PLATFORM = QwikCityPlatform> extends RequestEventBase<PLATFORM> \n```\n**Extends:** [RequestEventBase](#requesteventbase)<!-- -->&lt;PLATFORM&gt;\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\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\n[error](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: ErrorCodes, message: string) =&gt; ErrorResponse\n\n\n</td><td>\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`<!-- -->, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n</td></tr>\n<tr><td>\n\n[exit](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n() =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[html](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, html: string) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`<!-- -->. An `html()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[json](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, data: any) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`<!-- -->. A `json()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[locale](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(local?: string) =&gt; string\n\n\n</td><td>\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`<!-- -->:\n\n\n</td></tr>\n<tr><td>\n\n[redirect](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: RedirectCode, url: string) =&gt; [RedirectMessage](#redirectmessage)\n\n\n</td><td>\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n</td></tr>\n<tr><td>\n\n[send](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nSendMethod\n\n\n</td><td>\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[status](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode?: StatusCodes) =&gt; number\n\n\n</td><td>\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n</td></tr>\n<tr><td>\n\n[text](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, text: string) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`<!-- -->. An `text()` response can only be called once.\n\n\n</td></tr>\n</tbody></table>",
330+
"content": "```typescript\nexport interface RequestEventCommon<PLATFORM = QwikCityPlatform> extends RequestEventBase<PLATFORM> \n```\n**Extends:** [RequestEventBase](#requesteventbase)<!-- -->&lt;PLATFORM&gt;\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\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\n[error](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n&lt;T = any&gt;(statusCode: ErrorCodes, message: T) =&gt; [ServerError](#servererror)<!-- -->&lt;T&gt;\n\n\n</td><td>\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`<!-- -->, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n</td></tr>\n<tr><td>\n\n[exit](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n() =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[html](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, html: string) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`<!-- -->. An `html()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[json](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, data: any) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`<!-- -->. A `json()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[locale](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(local?: string) =&gt; string\n\n\n</td><td>\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`<!-- -->:\n\n\n</td></tr>\n<tr><td>\n\n[redirect](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: RedirectCode, url: string) =&gt; [RedirectMessage](#redirectmessage)\n\n\n</td><td>\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n</td></tr>\n<tr><td>\n\n[send](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nSendMethod\n\n\n</td><td>\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n</td></tr>\n<tr><td>\n\n[status](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode?: StatusCodes) =&gt; number\n\n\n</td><td>\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n</td></tr>\n<tr><td>\n\n[text](#)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n(statusCode: StatusCodes, text: string) =&gt; [AbortMessage](#abortmessage)\n\n\n</td><td>\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`<!-- -->. An `text()` response can only be called once.\n\n\n</td></tr>\n</tbody></table>",
331331
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/types.ts",
332332
"mdFile": "qwik-city.requesteventcommon.md"
333333
},
@@ -411,7 +411,7 @@
411411
}
412412
],
413413
"kind": "Class",
414-
"content": "```typescript\nexport declare class ServerError<T = Record<any, any>> extends Error \n```\n**Extends:** Error\n\n\n<table><thead><tr><th>\n\nConstructor\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[(constructor)(status, data)](#)\n\n\n</td><td>\n\n\n</td><td>\n\nConstructs a new instance of the `ServerError` class\n\n\n</td></tr>\n</tbody></table>\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\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\n[data](#servererror-data)\n\n\n</td><td>\n\n\n</td><td>\n\nT\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[status](#servererror-status)\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
414+
"content": "```typescript\nexport declare class ServerError<T = any> extends Error \n```\n**Extends:** Error\n\n\n<table><thead><tr><th>\n\nConstructor\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[(constructor)(status, data)](#)\n\n\n</td><td>\n\n\n</td><td>\n\nConstructs a new instance of the `ServerError` class\n\n\n</td></tr>\n</tbody></table>\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\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\n[data](#servererror-data)\n\n\n</td><td>\n\n\n</td><td>\n\nT\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[status](#servererror-status)\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
415415
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/middleware/request-handler/error-handler.ts",
416416
"mdFile": "qwik-city.servererror.md"
417417
},

packages/docs/src/routes/api/qwik-city-middleware-request-handler/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,7 +1216,7 @@ Description
12161216
12171217
</td><td>
12181218
1219-
(statusCode: ErrorCodes, message: string) =&gt; ErrorResponse
1219+
&lt;T = any&gt;(statusCode: ErrorCodes, message: T) =&gt; [ServerError](#servererror)&lt;T&gt;
12201220
12211221
</td><td>
12221222
@@ -1465,7 +1465,7 @@ export interface ResolveValue
14651465
## ServerError
14661466
14671467
```typescript
1468-
export declare class ServerError<T = Record<any, any>> extends Error
1468+
export declare class ServerError<T = any> extends Error
14691469
```
14701470
14711471
**Extends:** Error
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
title: Error handling | Qwik City
3+
contributors:
4+
- DustinJSilk
5+
updated_at: '2025-01-11T18:00:00Z'
6+
created_at: '2025-01-11T18:00:00Z'
7+
---
8+
9+
# Error handling
10+
11+
When an error is thrown in a loader or `server$` function, a 500 error is returned to the client along with the error. This is useful during development but isn't always desirable for production systems. Qwik provides the tools necessary to customise how errors are handled.
12+
13+
Throwing a `ServerError` instance allows you to return custom errors to the browser with a different status code and serialised data.
14+
15+
> Loaders also provide a helper function on the event object to easily create new ServerErrors.
16+
17+
```tsx
18+
// Throw ServerErrors from a routerLoader$
19+
const useProduct = routeLoader$(async (ev) => {
20+
const product = await fetch('api/product/1')
21+
22+
if (!product) {
23+
// Throw a 404 with a custom payload
24+
throw new ServerError(404, 'Product not found')
25+
26+
// Or use the existing helper function
27+
throw ev.error(404, 'Product not found')
28+
}
29+
30+
return product
31+
})
32+
33+
// Throw ServerErrors from a server$
34+
const getPrices = server$(() => {
35+
if (!isAuthenticated()) {
36+
throw new ServerError(401, { code: 401 })
37+
}
38+
39+
return fetch('api/product/1/prices')
40+
})
41+
42+
export default component$(() => {
43+
const product = useProduct()
44+
45+
useVisibleTask(() => {
46+
getPrices()
47+
.then()
48+
.catch(err => {
49+
// The payload from a ServerError is deserialised as the error caught in the client
50+
if (err.code === 401) {
51+
// Navigate to login page
52+
}
53+
54+
// Show generic error
55+
})
56+
})
57+
58+
return <div>Product page</div>
59+
})
60+
```
61+
62+
## Error interceptor
63+
64+
Intercepting errors with middleware has a few usecases: you might want to hide error details in production systems, add structured error logging, or map the error status codes from RPC API calls to HTTP status codes. This is all achieveable with middleware in a `plugin` file.
65+
66+
```tsx
67+
// src/routes/[email protected]
68+
import { type RequestHandler } from '@builder.io/qwik-city'
69+
import { RedirectMessage } from '@builder.io/qwik-city/middleware/request-handler'
70+
import { isDev } from '@builder.io/qwik/build'
71+
72+
export const onRequest: RequestHandler = async ({ next }) => {
73+
try {
74+
return await next();
75+
} catch (err) {
76+
// Pass through 3xx redirects
77+
if (isRedirectMessage(err)) {
78+
throw err
79+
}
80+
81+
// Pass through ServerErrors
82+
if (isServerError(err)) {
83+
throw err
84+
}
85+
86+
// Log unknown errors
87+
console.error('unknown error', err)
88+
89+
if (isDev) {
90+
throw err
91+
} else {
92+
throw new ServerError(500, 'Internal server error');
93+
}
94+
}
95+
};
96+
97+
function isServerError(err: unknown): err is ServerError {
98+
return (
99+
err instanceof ServerError ||
100+
// This is required for dev environments due to an issue with vite: https://github.com/vitejs/vite/issues/3910
101+
(isDev && err instanceof Error && err.constructor.name === "ServerError")
102+
);
103+
}
104+
105+
function isRedirectMessage(err: unknown): err is RedirectMessage {
106+
return (
107+
err instanceof RedirectMessage ||
108+
// This is required for dev environments due to an issue with vite: https://github.com/vitejs/vite/issues/3910
109+
(isDev && err instanceof Error && err.constructor.name === "RedirectMessage")
110+
);
111+
}
112+
```

packages/docs/src/routes/docs/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Endpoints](</docs/(qwikcity)/endpoints/index.mdx>)
3232
- [Middleware](</docs/(qwikcity)/middleware/index.mdx>)
3333
- [server$](</docs/(qwikcity)/server$/index.mdx>)
34+
- [Error handling](</docs/(qwikcity)/error-handling/index.mdx>)
3435
- [Re-exporting loaders](/docs/(qwikcity)/re-exporting-loaders/index.mdx)
3536
- [Caching](</docs/(qwikcity)/caching/index.mdx>)
3637
- [HTML attributes](</docs/(qwikcity)/html-attributes/index.mdx>)

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ export interface RequestEventBase<PLATFORM = QwikCityPlatform> {
126126
// @public (undocumented)
127127
export interface RequestEventCommon<PLATFORM = QwikCityPlatform> extends RequestEventBase<PLATFORM> {
128128
// Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts
129-
// Warning: (ae-forgotten-export) The symbol "ErrorResponse" needs to be exported by the entry point index.d.ts
130-
readonly error: (statusCode: ErrorCodes, message: string) => ErrorResponse;
129+
readonly error: <T = any>(statusCode: ErrorCodes, message: T) => ServerError<T>;
131130
// (undocumented)
132131
readonly exit: () => AbortMessage;
133132
readonly html: (statusCode: StatusCodes, html: string) => AbortMessage;
@@ -176,7 +175,7 @@ export interface ResolveValue {
176175
}
177176

178177
// @public (undocumented)
179-
export class ServerError<T = Record<any, any>> extends Error {
178+
export class ServerError<T = any> extends Error {
180179
constructor(status: number, data: T);
181180
// (undocumented)
182181
data: T;

packages/qwik-city/src/middleware/request-handler/error-handler.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
/** @public */
2-
export class ServerError<T = Record<any, any>> extends Error {
2+
export class ServerError<T = any> extends Error {
33
constructor(
44
public status: number,
55
public data: T
66
) {
7-
super();
7+
super(typeof data === 'string' ? data : undefined);
88
}
99
}
1010

11-
export class ErrorResponse extends Error {
11+
/** @deprecated */
12+
export class ErrorResponse extends ServerError {
1213
constructor(
1314
public status: number,
1415
message?: string
1516
) {
16-
super(message);
17+
super(status, message);
1718
}
1819
}
1920

0 commit comments

Comments
 (0)