@zerotal/arch 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Flow Lifecycle Hooks
|
|
3
|
+
description: Where to run code as a component mounts, updates, and tears down.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lifecycle Hooks
|
|
7
|
+
|
|
8
|
+
Lifecycle hooks let you run code at precise points in a component's life — on first load, on each WebSocket round-trip, before and after actions, around the render cycle, and on errors. They run server-side and have full access to the database, models, and services.
|
|
9
|
+
|
|
10
|
+
## Hook reference
|
|
11
|
+
|
|
12
|
+
| Hook | `GET` (initial) | WebSocket (subsequent) |
|
|
13
|
+
| ----------------------------- | --------------- | ------------------------------- |
|
|
14
|
+
| `onBoot()` | ✓ | ✓ |
|
|
15
|
+
| `onMount()` | ✓ | only if `this.refresh()` called |
|
|
16
|
+
| `onHydrate()` | — | ✓ |
|
|
17
|
+
| `onUpdating(prop, val, key?)` | — | ✓ (per client write) |
|
|
18
|
+
| `onUpdated(prop, val, key?)` | — | ✓ (per client write) |
|
|
19
|
+
| `action()` | — | ✓ (the invoked method) |
|
|
20
|
+
| `onUpdate()` | — | ✓ (once, after action) |
|
|
21
|
+
| `onRendering()` | ✓ | ✓ |
|
|
22
|
+
| `render()` | ✓ | ✓ |
|
|
23
|
+
| `onRendered(html)` | ✓ | ✓ |
|
|
24
|
+
| `onDehydrate()` | ✓ | ✓ |
|
|
25
|
+
| `onError(error)` | — | ✓ (on throw) |
|
|
26
|
+
|
|
27
|
+
## Request flow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
GET /page (initial render) WebSocket action frame (subsequent)
|
|
31
|
+
│ │
|
|
32
|
+
├─ onBoot(ctx) ├─ onBoot(ctx)
|
|
33
|
+
├─ onMount(ctx) ├─ onHydrate() ← state restored from snapshot
|
|
34
|
+
├─ onRendering() ├─ onUpdating/onUpdated ← per client-written property
|
|
35
|
+
├─ render() ├─ action() ← the invoked exposed method
|
|
36
|
+
├─ onRendered(html) ├─ onUpdate() ← once, after action
|
|
37
|
+
└─ onDehydrate() ├─ onRendering()
|
|
38
|
+
├─ render()
|
|
39
|
+
├─ onRendered(html)
|
|
40
|
+
└─ onDehydrate()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`onMount()` is absent from the WebSocket column because it only runs on the initial `GET` — unless the action called `this.refresh()`, which re-inserts it into the WebSocket flow.
|
|
44
|
+
|
|
45
|
+
## Child component initialisation
|
|
46
|
+
|
|
47
|
+
A child gets its data from its parent, not from the URL. Each prop lands on the field of the same name before `onBoot()` and `onMount()` run, so the field's initialiser is its default and a hook can use the value straight away:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
export class CounterWidget extends Component {
|
|
51
|
+
@locked label: string = "Count"; // ← <CounterWidget label="Views" />
|
|
52
|
+
@locked step: number = 1; // ← defaults to 1 when the parent omits it
|
|
53
|
+
@expose count: number = 0;
|
|
54
|
+
|
|
55
|
+
@expose increment(): void {
|
|
56
|
+
this.count += this.step;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override async render() {
|
|
60
|
+
return (
|
|
61
|
+
<div class="card">
|
|
62
|
+
<p class="text-sm text-gray-500">{this.label}</p>
|
|
63
|
+
<p class="text-3xl font-bold">{this.count}</p>
|
|
64
|
+
<button onClick={this.increment}>+ {this.step}</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Props that must survive round-trips need to be `@locked` — that keeps them in the snapshot, and the value is then restored from there rather than re-assigned. A `@reactive` prop is different: the parent re-pushes it whenever it changes, so the child sees the new value on the next round-trip.
|
|
72
|
+
|
|
73
|
+
A child receives the request `HttpContext` in `onBoot(ctx)` / `onMount(ctx)` like any page, so it can still read the session or the signed-in user. What it never reads is the URL's segments — those fill the page, not the pieces inside it, which is what lets a child be dropped anywhere.
|
|
74
|
+
|
|
75
|
+
## Per-request setup
|
|
76
|
+
|
|
77
|
+
Runs on **every** request: the initial `GET` and every WebSocket update. Use it for setup that must be fresh on every round-trip — resolving the authenticated user from context, initialising i18n, wiring up per-request services:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { request } from "zerotal";
|
|
81
|
+
|
|
82
|
+
override async onBoot() {
|
|
83
|
+
const ctx = request();
|
|
84
|
+
this.currentUserId = ctx.user?.id ?? 0;
|
|
85
|
+
this.locale = ctx.string("locale", "en") ?? "en";
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`onBoot()` runs before both `onMount()` and `onHydrate()`, so state it sets is available in both.
|
|
90
|
+
|
|
91
|
+
Avoid expensive database queries in `onBoot()` — it fires on every round-trip, including fast UI interactions. Reserve those for `onMount()` or `onHydrate()`.
|
|
92
|
+
|
|
93
|
+
## Loading data
|
|
94
|
+
|
|
95
|
+
Runs once on the initial `GET` render, then is skipped on all subsequent WebSocket updates. It's the primary place to load data for the page.
|
|
96
|
+
|
|
97
|
+
It receives the route `HttpContext` — the same argument a controller action gets — so a page on a dynamic segment reads its [route-model binding](/docs/routing#route-model-binding) straight off `ctx.params` instead of querying for it:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
override async onMount({ params: { post } }: HttpContext<{ post: Post }>) {
|
|
101
|
+
this.post = post; // resolved by the router; a missing record 404s before this runs
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The context is passed to `onBoot()` too, but only the initial `GET` populates `ctx.params` — see the [warning in Routing](/docs/flow/routing#path-parameters). The argument is optional because a component can also be mounted outside a request (in a test, for example).
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
override async onMount() {
|
|
109
|
+
const [posts, user] = await Promise.all([
|
|
110
|
+
Post.query()
|
|
111
|
+
.where("status", "published")
|
|
112
|
+
.orderBy("created_at", "desc")
|
|
113
|
+
.limit(20)
|
|
114
|
+
.get(),
|
|
115
|
+
User.findOrFail(this.currentUserId),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
this.posts = posts;
|
|
119
|
+
this.user = user;
|
|
120
|
+
this.total = posts.length;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
To force `onMount()` to re-run during a WebSocket action — for example after creating a new record and wanting to reload the list — call `this.refresh()` inside the action:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
@expose async createPost(): Promise<void> {
|
|
128
|
+
await Post.create({ title: this.title, body: this.body });
|
|
129
|
+
this.title = "";
|
|
130
|
+
this.body = "";
|
|
131
|
+
this.refresh(); // triggers onMount() on this round-trip
|
|
132
|
+
this.flash("Post published.");
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Re-deriving state after hydration
|
|
137
|
+
|
|
138
|
+
Runs on every WebSocket round-trip, immediately after state is restored from the snapshot. Use it to re-derive transient or protected state that wasn't persisted in the snapshot:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
export class PostEditorPage extends Component {
|
|
142
|
+
@locked postId: number = 0; // persisted in snapshot
|
|
143
|
+
@transient post: Post | null = null; // NOT persisted — reset each round-trip
|
|
144
|
+
|
|
145
|
+
override async onHydrate() {
|
|
146
|
+
// Re-load the full Post model from the database using the persisted ID:
|
|
147
|
+
if (this.postId) {
|
|
148
|
+
this.post = await Post.findOrFail(this.postId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@expose async updateTitle(title: string): Promise<void> {
|
|
153
|
+
if (!this.post) return;
|
|
154
|
+
await this.post.fill({ title }).save();
|
|
155
|
+
this.flash("Title updated.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This is the correct pattern for holding live model instances on a component: persist only the ID in `@locked`, then re-query the model in `onHydrate()`. The model is always fresh from the database, never stale from a deserialized snapshot.
|
|
161
|
+
|
|
162
|
+
## Intercepting client writes
|
|
163
|
+
|
|
164
|
+
Fires **before** a client-written property value is applied to the component. Throw to reject the write — the value is discarded, an error is added, and the component re-renders:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
override async onUpdating(prop: string, value: unknown, key?: string) {
|
|
168
|
+
// Prevent role escalation
|
|
169
|
+
if (prop === "role" && value === "super_admin") {
|
|
170
|
+
throw new Error("You cannot assign this role.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Reject negative numbers for any numeric prop
|
|
174
|
+
if (typeof value === "number" && value < 0) {
|
|
175
|
+
throw new Error(`${prop} cannot be negative.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`key` is present when the property is an array or object and the client updated a nested path (e.g., `form.email` — `prop` is `"form"`, `key` is `"email"`).
|
|
181
|
+
|
|
182
|
+
## Reacting after client writes
|
|
183
|
+
|
|
184
|
+
Fires **after** a client-written property is applied. Use it to normalise values, enforce computed side-effects, or trigger cascading updates:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
override async onUpdated(prop: string, value: unknown) {
|
|
188
|
+
if (prop === "categoryId") {
|
|
189
|
+
// When the category changes, reload the subcategories
|
|
190
|
+
this.subcategories = await Category.where("parent_id", value).get();
|
|
191
|
+
this.subcategoryId = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Per-property update hooks
|
|
197
|
+
|
|
198
|
+
Instead of branching on `prop` inside `onUpdating`/`onUpdated`, define a per-property method named `onUpdating<PropName>` or `onUpdated<PropName>` (Pascal-cased). Flow calls it automatically and keeps the generic fallback as a catch-all:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
@expose username = "";
|
|
202
|
+
@expose email = "";
|
|
203
|
+
@expose tags: string[] = [];
|
|
204
|
+
|
|
205
|
+
// Called only when the client writes to `username`:
|
|
206
|
+
async onUpdatedUsername(value: string) {
|
|
207
|
+
this.username = value.toLowerCase().trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Called only when the client writes to `email`:
|
|
211
|
+
async onUpdatingEmail(value: string) {
|
|
212
|
+
if (!value.includes("@")) {
|
|
213
|
+
throw new Error("Not a valid email address.");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Generic fallback — fired for any property not handled above:
|
|
218
|
+
async onUpdating(prop: string, value: unknown) {
|
|
219
|
+
if (prop === "role" && value === "super_admin") {
|
|
220
|
+
throw new Error("You cannot set this role.");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The per-property form is cleaner and TypeScript-friendly: the parameter type matches the property type rather than `unknown`.
|
|
226
|
+
|
|
227
|
+
## After action, before render
|
|
228
|
+
|
|
229
|
+
Runs once after the invoked action completes, before the render cycle. Use it to apply cross-cutting logic that should happen after any action:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
override async onUpdate() {
|
|
233
|
+
// Always log the current state to the audit trail after any action:
|
|
234
|
+
await AuditLog.create({
|
|
235
|
+
component: this.constructor.name,
|
|
236
|
+
userId: this.currentUserId,
|
|
237
|
+
snapshotSum: JSON.stringify(this).length,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Unlike `onUpdated` (which fires per property, before the action), `onUpdate()` fires once per request, after the action returns.
|
|
243
|
+
|
|
244
|
+
## Before render
|
|
245
|
+
|
|
246
|
+
Runs immediately before `render()` on every request (initial and WebSocket). Use it for template-level setup that shouldn't be in `render()` itself — resolving shared view data, picking a layout variant, etc.:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
override async onRendering() {
|
|
250
|
+
// Decide which layout variant to use based on the user's subscription
|
|
251
|
+
if (this.user?.isPro) {
|
|
252
|
+
this.layoutVariant = "pro";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Avoid async database calls here unless truly necessary — `onMount()` and `onHydrate()` are the right places to load data. `onRendering()` is for lightweight, synchronous prep.
|
|
258
|
+
|
|
259
|
+
## After render
|
|
260
|
+
|
|
261
|
+
Receives the rendered HTML string. Use it to post-process the output, measure render time, or send the HTML to a cache:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
override async onRendered(html: string) {
|
|
265
|
+
// Log very long renders for investigation
|
|
266
|
+
if (html.length > 100_000) {
|
|
267
|
+
await Logger.warn("flow.render.large", {
|
|
268
|
+
component: this.constructor.name,
|
|
269
|
+
bytes: html.length,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The `html` parameter is the raw HTML of this component only — not the full page. Do not mutate it here; return value is ignored.
|
|
276
|
+
|
|
277
|
+
## Normalising before snapshot
|
|
278
|
+
|
|
279
|
+
Runs just before the component state is serialised into the snapshot at the end of every request. Use it to strip sensitive or ephemeral state that shouldn't be persisted:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
override async onDehydrate() {
|
|
283
|
+
// Never persist raw upload paths between round-trips
|
|
284
|
+
this.tempUploadPath = null;
|
|
285
|
+
|
|
286
|
+
// Strip the full user object — only the ID needs to survive
|
|
287
|
+
this.userObject = null;
|
|
288
|
+
|
|
289
|
+
// Trim large arrays before snapshot to keep payload size down
|
|
290
|
+
if (this.logBuffer.length > 100) {
|
|
291
|
+
this.logBuffer = this.logBuffer.slice(-100);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
After `onDehydrate()`, the snapshot is signed and sent to the browser as an encrypted opaque blob. The next round-trip restores it before `onHydrate()` runs.
|
|
297
|
+
|
|
298
|
+
## Custom error handling
|
|
299
|
+
|
|
300
|
+
Called when an `@expose`d action throws an unhandled error. The default behaviour flashes the error message with level `"error"`. Override to log to an error tracker or display a custom message:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
override async onError(error: Error) {
|
|
304
|
+
// Log to your error tracker
|
|
305
|
+
await Sentry.captureException(error, {
|
|
306
|
+
extra: {
|
|
307
|
+
component: this.constructor.name,
|
|
308
|
+
userId: this.currentUserId,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Show a user-friendly message instead of the raw error text
|
|
313
|
+
this.flash("Something went wrong. Our team has been notified.", "error");
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
If you want some errors to propagate normally and only handle specific types:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
override async onError(error: Error) {
|
|
321
|
+
if (error instanceof DatabaseConnectionError) {
|
|
322
|
+
this.flash("Database is temporarily unavailable. Please try again.", "error");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Re-throw everything else to get the default flash behaviour
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`onError()` does not run during the initial `GET` render — if `onMount()` throws, the error propagates to the HTTP layer and results in a 500 response. It only runs during WebSocket action frames.
|
|
332
|
+
|
|
333
|
+
## Next steps
|
|
334
|
+
|
|
335
|
+
- [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
|
|
336
|
+
- [Reference](/docs/flow/references) — every decorator, prop, and directive in one table.
|