@venizia/ignis-docs 0.2.0 → 0.2.1-1
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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +8 -4
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +247 -153
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +58 -218
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,52 +1,106 @@
|
|
|
1
1
|
# Error Handling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every error in IGNIS ends up as one shape: `statusCode`, `message`, `normalized`, `requestId`, and `details`. `getError` builds that shape at the throw site. A global handler catches whatever you don't.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```typescript
|
|
6
|
+
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
7
|
+
|
|
8
|
+
throw getError({
|
|
9
|
+
statusCode: HTTP.ResultCodes.RS_4.NotFound,
|
|
10
|
+
message: 'User not found',
|
|
11
|
+
});
|
|
12
|
+
```
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|-----------|-------------|
|
|
9
|
-
| **Fail Fast** | Detect and report errors as early as possible |
|
|
10
|
-
| **Don't Swallow** | Never catch errors without logging or re-throwing |
|
|
11
|
-
| **User-Friendly** | Return clear, actionable messages to clients |
|
|
12
|
-
| **Debuggable** | Include context for debugging in logs |
|
|
14
|
+
## Find what you need
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
| You want to | Go to |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Throw an error with debugging context | [Throw a structured error](#throw-a-structured-error) |
|
|
19
|
+
| Add an i18n error a client can branch on | [Catalog a domain error](#catalog-a-domain-error) |
|
|
20
|
+
| Pick the right HTTP status code | [Choose the right status code](#choose-the-right-status-code) |
|
|
21
|
+
| Handle errors in a service, controller, or repository | [Handle errors by layer](#handle-errors-by-layer) |
|
|
22
|
+
| Customize the global error response | [Customize the global handler](#customize-the-global-handler) |
|
|
23
|
+
| Read the exact JSON a client receives | [The error response shape](#the-error-response-shape) |
|
|
24
|
+
| Log an error without losing the stack | [Log errors](#log-errors) |
|
|
25
|
+
| Handle a rejected promise or a fire-and-forget call | [Handle async errors](#handle-async-errors) |
|
|
26
|
+
| Roll back a transaction safely | [Roll back a transaction on failure](#roll-back-a-transaction-on-failure) |
|
|
27
|
+
| Parse an error response on the client | [Parse errors on the client](#parse-errors-on-the-client) |
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
## Throw a structured error
|
|
30
|
+
|
|
31
|
+
`getError` is the one function every throw in IGNIS goes through. Give it a status code and a message, and it returns an `ApplicationError` ready to throw.
|
|
17
32
|
|
|
18
33
|
```typescript
|
|
19
34
|
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
20
35
|
|
|
21
|
-
//
|
|
36
|
+
// Error with context - any key the framework does not model lands in `extra`
|
|
22
37
|
throw getError({
|
|
23
|
-
statusCode: HTTP.ResultCodes.RS_4.
|
|
24
|
-
message: '
|
|
38
|
+
statusCode: HTTP.ResultCodes.RS_4.BadRequest,
|
|
39
|
+
message: 'Invalid request',
|
|
40
|
+
details: { field: 'email', reason: 'Must be a valid email address' },
|
|
25
41
|
});
|
|
42
|
+
// -> error.extra.details
|
|
26
43
|
|
|
27
|
-
//
|
|
44
|
+
// The same thing, explicit. Prefer this when the context could be mistaken for a field.
|
|
28
45
|
throw getError({
|
|
29
46
|
statusCode: HTTP.ResultCodes.RS_4.BadRequest,
|
|
30
47
|
message: 'Invalid request',
|
|
31
|
-
details: {
|
|
32
|
-
field: 'email',
|
|
33
|
-
reason: 'Must be a valid email address',
|
|
34
|
-
},
|
|
48
|
+
extra: { details: { field: 'email' } },
|
|
35
49
|
});
|
|
36
50
|
|
|
37
|
-
//
|
|
51
|
+
// Wrapping a lower-level failure - `cause` reaches the native Error.cause
|
|
38
52
|
throw getError({
|
|
39
53
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
40
54
|
message: '[UserService][create] Database connection failed',
|
|
41
|
-
|
|
55
|
+
cause: error,
|
|
56
|
+
extra: { userId: requestedId },
|
|
42
57
|
});
|
|
43
58
|
```
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
Any key `getError` does not model rides into `extra`. That's how a throw site attaches context the framework knows nothing about.
|
|
46
61
|
|
|
47
|
-
|
|
62
|
+
> [!NOTE]
|
|
63
|
+
> The trade-off: a **misspelling** rides the same path. `getError({ message, statuscode: 503 })` compiles. `statusCode` stays `400`, and `503` sits in `extra.statuscode`. The framework can't tell your context from your typo - spell option keys carefully.
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
## Catalog a domain error
|
|
66
|
+
|
|
67
|
+
A free-form `getError` call like that is right for a failure with no i18n code: an invariant, a misconfiguration, a seed guard. Nobody translates `'[UserService][create] Database connection failed'`.
|
|
68
|
+
|
|
69
|
+
But a **domain** failure - one a client localizes and branches on - belongs in a catalog instead. Retyping its code and status at each throw is how two call sites end up raising `category.create.duplicate_name` and `category.duplicate_name` for the same thing.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { ErrorScopes, getError, HTTP } from '@venizia/ignis-helpers';
|
|
73
|
+
import type { TErrorDefinition, TRegisterErrors } from '@venizia/ignis-helpers';
|
|
74
|
+
|
|
75
|
+
export const UserErrors = {
|
|
76
|
+
CREATE_DUPLICATE_EMAIL: {
|
|
77
|
+
message: {
|
|
78
|
+
text: 'An account with %{email} already exists.',
|
|
79
|
+
code: 'server.core.user.create.duplicate_email',
|
|
80
|
+
},
|
|
81
|
+
statusCode: HTTP.ResultCodes.RS_4.Conflict,
|
|
82
|
+
category: ErrorScopes.VALIDATION,
|
|
83
|
+
description: 'Sign-up rejected because the email is already registered.',
|
|
84
|
+
},
|
|
85
|
+
} as const satisfies Record<string, TErrorDefinition>;
|
|
86
|
+
|
|
87
|
+
declare module '@venizia/ignis-helpers' {
|
|
88
|
+
interface IErrorKeyRegistry extends TRegisterErrors<typeof UserErrors> {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// At every throw site:
|
|
92
|
+
throw getError({ error: UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Pass the definition as `error` - never spread it. `getError({ ...UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } })` looks equivalent. It isn't.
|
|
96
|
+
|
|
97
|
+
Spreading skips the `error:` field entirely. `category` and `description` are fields only a definition models, so they fall into `extra` instead of staying structured. `statusCode` and `message` still resolve correctly, but only because their shapes happen to match what `getError` expects standalone - nothing catches the rest for you.
|
|
98
|
+
|
|
99
|
+
See the [Error helper reference](/extensions/helpers/error/) for the full surface.
|
|
100
|
+
|
|
101
|
+
## Choose the right status code
|
|
102
|
+
|
|
103
|
+
| Code | Constant | Use when |
|
|
50
104
|
|------|----------|----------|
|
|
51
105
|
| 400 | `RS_4.BadRequest` | Invalid input format, missing required fields, database constraint violations (auto-handled) |
|
|
52
106
|
| 401 | `RS_4.Unauthorized` | Missing or invalid authentication |
|
|
@@ -59,13 +113,15 @@ Use the correct status code for each error type:
|
|
|
59
113
|
| 502 | `RS_5.BadGateway` | External service failed |
|
|
60
114
|
| 503 | `RS_5.ServiceUnavailable` | Service temporarily down |
|
|
61
115
|
|
|
62
|
-
:::tip Automatic
|
|
63
|
-
|
|
116
|
+
:::tip Automatic database error handling
|
|
117
|
+
The global error middleware converts constraint violations to HTTP 400 on its own, no try/catch needed - SQLSTATE class `22` (data exception), `23` (integrity constraint: unique, foreign key, not null, check, exclusion), and `44` (WITH CHECK OPTION violation). Transient conflicts (`40001` serialization failure, `40P01` deadlock) become HTTP 409 with a retryable message instead. Other classes (for example, class `42` undefined column) stay 500, and production responses are sanitized - see [Repository layer](#repository-layer).
|
|
64
118
|
:::
|
|
65
119
|
|
|
66
|
-
##
|
|
120
|
+
## Handle errors by layer
|
|
121
|
+
|
|
122
|
+
### Service layer
|
|
67
123
|
|
|
68
|
-
|
|
124
|
+
A service decides what's wrong and throws with `getError`. It doesn't catch what it can't handle.
|
|
69
125
|
|
|
70
126
|
```typescript
|
|
71
127
|
import { BaseService } from '@venizia/ignis';
|
|
@@ -90,9 +146,9 @@ export class UserService extends BaseService {
|
|
|
90
146
|
try {
|
|
91
147
|
await this.emailService.sendWelcome(data.email);
|
|
92
148
|
} catch (error) {
|
|
93
|
-
// Log but don't fail user creation
|
|
149
|
+
// Log but don't fail user creation - never a silent catch
|
|
94
150
|
this.logger.error('[createUser] Failed to send welcome email | email: %s | error: %s',
|
|
95
|
-
data.email, error
|
|
151
|
+
data.email, error);
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
// create returns { count, data }
|
|
@@ -117,9 +173,11 @@ export class UserService extends BaseService {
|
|
|
117
173
|
}
|
|
118
174
|
```
|
|
119
175
|
|
|
120
|
-
|
|
176
|
+
Notice `findOne` and `findById` return the record or `null` - there's no separate "not found" exception to catch. You check for `null` and throw.
|
|
121
177
|
|
|
122
|
-
|
|
178
|
+
### Controller layer
|
|
179
|
+
|
|
180
|
+
Controllers stay thin. Call the service, return the response, and let the global handler catch what the service throws.
|
|
123
181
|
|
|
124
182
|
```typescript
|
|
125
183
|
import { BaseRestController, controller, get, post } from '@venizia/ignis';
|
|
@@ -149,37 +207,52 @@ export class UserController extends BaseRestController {
|
|
|
149
207
|
}
|
|
150
208
|
```
|
|
151
209
|
|
|
152
|
-
### Repository
|
|
210
|
+
### Repository layer
|
|
153
211
|
|
|
154
|
-
|
|
212
|
+
The global middleware already converts constraint violations for you (see [Choose the right status code](#choose-the-right-status-code)), so most repository methods need no try/catch at all. Skip it unless you want a message tailored to one specific constraint.
|
|
155
213
|
|
|
156
214
|
**Non-production** returns the full driver context for debugging:
|
|
157
215
|
|
|
158
216
|
```json
|
|
159
217
|
{
|
|
160
218
|
"message": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
|
|
161
|
-
"messageCode": "core.system_error",
|
|
162
219
|
"statusCode": 400,
|
|
163
|
-
"
|
|
220
|
+
"normalized": {
|
|
221
|
+
"text": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
|
|
222
|
+
"code": "core.system_error",
|
|
223
|
+
"args": {}
|
|
224
|
+
},
|
|
225
|
+
"requestId": "abc123",
|
|
226
|
+
"details": {
|
|
227
|
+
"url": "http://localhost:3000/users",
|
|
228
|
+
"path": "/users",
|
|
229
|
+
"stack": "Error: Unique constraint violation\n at ..."
|
|
230
|
+
}
|
|
164
231
|
}
|
|
165
232
|
```
|
|
166
233
|
|
|
167
234
|
:::warning Production sanitizes database internals
|
|
168
|
-
In production the message is the **base message only
|
|
235
|
+
In production the message is the **base message only**. `Detail:` (which echoes row values like emails), `Table:`, and `Constraint:` are stripped, and `details.stack`/`details.cause` are omitted. Unexpected (non-client) database errors and connection failures return a generic `"Internal Server Error"`, so SQL, schema names, and connection host/port never leak. Use `requestId` and server logs to diagnose.
|
|
169
236
|
|
|
170
237
|
```json
|
|
171
|
-
{
|
|
238
|
+
{
|
|
239
|
+
"message": "Unique constraint violation",
|
|
240
|
+
"statusCode": 400,
|
|
241
|
+
"normalized": { "text": "Unique constraint violation", "code": "core.system_error", "args": {} },
|
|
242
|
+
"requestId": "abc123",
|
|
243
|
+
"details": { "url": "http://localhost:3000/users", "path": "/users" }
|
|
244
|
+
}
|
|
172
245
|
```
|
|
173
246
|
:::
|
|
174
247
|
|
|
175
|
-
|
|
248
|
+
If you need a custom message for one constraint, catch it explicitly and re-throw everything else:
|
|
176
249
|
|
|
177
250
|
```typescript
|
|
178
|
-
import { DefaultCRUDRepository } from '@venizia/ignis';
|
|
251
|
+
import { DefaultCRUDRepository, type TCount } from '@venizia/ignis';
|
|
179
252
|
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
180
253
|
|
|
181
254
|
export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
|
|
182
|
-
async createWithCustomError(data: TCreateUser): Promise<
|
|
255
|
+
async createWithCustomError(data: TCreateUser): Promise<TCount & { data: TUser }> {
|
|
183
256
|
try {
|
|
184
257
|
return await this.create({ data });
|
|
185
258
|
} catch (error) {
|
|
@@ -196,43 +269,45 @@ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
|
|
|
196
269
|
}
|
|
197
270
|
```
|
|
198
271
|
|
|
199
|
-
##
|
|
272
|
+
## Customize the global handler
|
|
200
273
|
|
|
201
|
-
IGNIS
|
|
274
|
+
IGNIS wires a built-in handler by default. `AppErrorMiddleware` from `@venizia/ignis` is a class, registered as `new AppErrorMiddleware({ logger, rootKey }).value()`, and `value()` returns the Hono `ErrorHandler`. You rarely need to replace it. When you do, keep the same response contract:
|
|
202
275
|
|
|
203
276
|
```typescript
|
|
204
277
|
import { BaseApplication } from '@venizia/ignis';
|
|
205
|
-
import {
|
|
278
|
+
import { isApplicationError, MessageCode } from '@venizia/ignis-helpers';
|
|
206
279
|
|
|
207
280
|
export class Application extends BaseApplication {
|
|
208
281
|
override setupMiddlewares(): void {
|
|
209
282
|
super.setupMiddlewares();
|
|
210
283
|
|
|
211
|
-
// Custom error handler (optional)
|
|
212
|
-
this.server
|
|
284
|
+
// Custom error handler (optional) - the default AppErrorMiddleware already does this.
|
|
285
|
+
// `this.server` is `{ hono, runtime, instance }` - reach the Hono app via getServer().
|
|
286
|
+
this.getServer().onError((error, c) => {
|
|
213
287
|
const requestId = c.get('requestId') ?? 'unknown';
|
|
214
288
|
|
|
215
289
|
// Log all errors
|
|
216
|
-
this.logger.error('[%s] Error | %s', requestId, error
|
|
290
|
+
this.logger.error('[%s] Error | %s', requestId, error);
|
|
217
291
|
|
|
218
|
-
// Handle known application errors
|
|
219
|
-
|
|
292
|
+
// Handle known application errors - isApplicationError(), never `instanceof
|
|
293
|
+
// ApplicationError` (unreliable across package boundaries)
|
|
294
|
+
if (isApplicationError(error)) {
|
|
220
295
|
return c.json({
|
|
221
296
|
statusCode: error.statusCode,
|
|
222
297
|
message: error.message,
|
|
223
|
-
|
|
224
|
-
|
|
298
|
+
normalized: error.normalized, // { text, code, args } - never undefined
|
|
299
|
+
extra: error.extra,
|
|
225
300
|
requestId,
|
|
226
|
-
}, error.statusCode as
|
|
301
|
+
}, error.statusCode as Parameters<typeof c.json>[1]);
|
|
227
302
|
}
|
|
228
303
|
|
|
229
|
-
// Handle Zod validation errors
|
|
304
|
+
// Handle Zod validation errors - Zod v4 exposes `issues`, not `errors`
|
|
230
305
|
if (error.name === 'ZodError') {
|
|
231
306
|
return c.json({
|
|
232
307
|
statusCode: 422,
|
|
233
308
|
message: 'Validation failed',
|
|
234
|
-
|
|
235
|
-
details: { cause: error.
|
|
309
|
+
normalized: { text: 'Validation failed', code: MessageCode.DEFAULT, args: {} },
|
|
310
|
+
details: { cause: error.issues },
|
|
236
311
|
requestId,
|
|
237
312
|
}, 422);
|
|
238
313
|
}
|
|
@@ -241,7 +316,7 @@ export class Application extends BaseApplication {
|
|
|
241
316
|
return c.json({
|
|
242
317
|
statusCode: 500,
|
|
243
318
|
message: 'Internal server error',
|
|
244
|
-
|
|
319
|
+
normalized: { text: 'Internal server error', code: MessageCode.DEFAULT, args: {} },
|
|
245
320
|
requestId,
|
|
246
321
|
}, 500);
|
|
247
322
|
});
|
|
@@ -249,59 +324,67 @@ export class Application extends BaseApplication {
|
|
|
249
324
|
}
|
|
250
325
|
```
|
|
251
326
|
|
|
252
|
-
##
|
|
253
|
-
|
|
254
|
-
All errors should follow a consistent format:
|
|
327
|
+
## The error response shape
|
|
255
328
|
|
|
256
329
|
```typescript
|
|
257
330
|
interface ErrorResponse {
|
|
258
331
|
statusCode: number;
|
|
259
332
|
message: string;
|
|
260
|
-
|
|
333
|
+
normalized: {
|
|
334
|
+
text: string;
|
|
335
|
+
code: string; // stable, localizable code (validation: from params.code or the raw Zod code)
|
|
336
|
+
args: Record<string, unknown>;
|
|
337
|
+
};
|
|
261
338
|
requestId: string;
|
|
262
339
|
extra?: Record<string, unknown>; // structured context attached via getError(...)
|
|
263
|
-
details
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}>;
|
|
340
|
+
details: {
|
|
341
|
+
url: string;
|
|
342
|
+
path: string;
|
|
343
|
+
stack?: string; // non-production only
|
|
344
|
+
cause?: unknown; // non-production only, or the Zod issue list for 422s
|
|
269
345
|
[key: string]: unknown;
|
|
270
346
|
};
|
|
271
347
|
}
|
|
272
348
|
```
|
|
273
349
|
|
|
274
|
-
|
|
350
|
+
There's no top-level `messageCode` - read `normalized.code`. `extra` never mirrors `messageArgs`; the resolved interpolation values live at `normalized.args`.
|
|
351
|
+
|
|
352
|
+
**400 Bad Request:**
|
|
275
353
|
|
|
276
354
|
```json
|
|
277
|
-
// 400 Bad Request
|
|
278
355
|
{
|
|
279
|
-
"statusCode": 400,
|
|
280
356
|
"message": "Invalid request body",
|
|
281
|
-
"
|
|
282
|
-
"
|
|
357
|
+
"statusCode": 400,
|
|
358
|
+
"normalized": { "text": "Invalid request body", "code": "core.system_error", "args": {} },
|
|
359
|
+
"requestId": "abc123",
|
|
360
|
+
"details": { "url": "http://localhost:3000/users", "path": "/users" }
|
|
283
361
|
}
|
|
362
|
+
```
|
|
284
363
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
364
|
+
**404 Not Found:** extra keys passed to `getError(...)` (for example `details`) surface under the top-level `extra`. The top-level `details` object is reserved for middleware context (url, path, stack, cause) - the two never share a key.
|
|
365
|
+
|
|
366
|
+
```json
|
|
288
367
|
{
|
|
289
|
-
"statusCode": 404,
|
|
290
368
|
"message": "User not found",
|
|
291
|
-
"
|
|
369
|
+
"statusCode": 404,
|
|
370
|
+
"normalized": { "text": "User not found", "code": "core.system_error", "args": {} },
|
|
292
371
|
"requestId": "abc123",
|
|
293
|
-
"extra": { "details": { "id": "user-uuid" } }
|
|
372
|
+
"extra": { "details": { "id": "user-uuid" } },
|
|
373
|
+
"details": { "url": "http://localhost:3000/users/user-uuid", "path": "/users/:id" }
|
|
294
374
|
}
|
|
375
|
+
```
|
|
295
376
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
377
|
+
**422 Validation Error:** `message` and `normalized.code` come from the first failing issue - its `params.code` if the schema set one, otherwise the raw Zod code (for example `invalid_type`, `too_small`). `normalized.args` is always empty for a Zod issue; the full list of issues stays in `details.cause`.
|
|
378
|
+
|
|
379
|
+
```json
|
|
299
380
|
{
|
|
300
|
-
"statusCode": 422,
|
|
301
381
|
"message": "Invalid email format",
|
|
302
|
-
"
|
|
382
|
+
"statusCode": 422,
|
|
383
|
+
"normalized": { "text": "Invalid email format", "code": "user.email.invalid", "args": {} },
|
|
303
384
|
"requestId": "abc123",
|
|
304
385
|
"details": {
|
|
386
|
+
"url": "http://localhost:3000/users",
|
|
387
|
+
"path": "/users",
|
|
305
388
|
"cause": [
|
|
306
389
|
{
|
|
307
390
|
"path": "email",
|
|
@@ -311,55 +394,50 @@ interface ErrorResponse {
|
|
|
311
394
|
]
|
|
312
395
|
}
|
|
313
396
|
}
|
|
314
|
-
|
|
315
|
-
// 500 Internal Error (production)
|
|
316
|
-
{
|
|
317
|
-
"statusCode": 500,
|
|
318
|
-
"message": "Internal server error",
|
|
319
|
-
"messageCode": "core.system_error",
|
|
320
|
-
"requestId": "abc123"
|
|
321
|
-
}
|
|
322
397
|
```
|
|
323
398
|
|
|
324
|
-
##
|
|
399
|
+
## Log errors
|
|
325
400
|
|
|
326
|
-
### `%s`,
|
|
401
|
+
### Use `%s`, never `%j`, for an Error
|
|
327
402
|
|
|
328
|
-
`message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both
|
|
403
|
+
`message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both. Always use `%s` to log an `Error` instance; reserve `%j`/`%o` for plain data objects.
|
|
329
404
|
|
|
330
405
|
```typescript
|
|
331
|
-
//
|
|
406
|
+
// Good - %s prints message + stack
|
|
332
407
|
this.logger.error('[createOrder] Failed | error: %s', error);
|
|
333
408
|
|
|
334
|
-
//
|
|
409
|
+
// Bad - %j drops the stack
|
|
335
410
|
this.logger.error('[createOrder] Failed | error: %j', error);
|
|
336
411
|
```
|
|
337
412
|
|
|
338
|
-
|
|
413
|
+
> [!NOTE]
|
|
414
|
+
> An `ApplicationError` logged with `%j` does show its message text, because the text rides inside the enumerable `normalized.text`. That's incidental, not a reprieve - the **stack** is still gone, which is the reason the rule exists. A plain `Error` under `%j` still logs little more than `{}`.
|
|
415
|
+
|
|
416
|
+
### What to log
|
|
339
417
|
|
|
340
418
|
```typescript
|
|
341
|
-
//
|
|
419
|
+
// Good - context for debugging. `%s` on the error itself carries message + stack
|
|
342
420
|
this.logger.error('[createOrder] Failed | userId: %s | orderId: %s | error: %s',
|
|
343
|
-
userId, orderId, error
|
|
344
|
-
|
|
345
|
-
// ✅ Good - Include stack trace for unexpected errors
|
|
346
|
-
this.logger.error('[createOrder] Unexpected error | %s', error.stack);
|
|
421
|
+
userId, orderId, error);
|
|
347
422
|
|
|
348
|
-
//
|
|
423
|
+
// Bad - no context, and `.message` throws the stack away
|
|
349
424
|
this.logger.error(error.message);
|
|
350
425
|
|
|
351
|
-
//
|
|
426
|
+
// Bad - sensitive data
|
|
352
427
|
this.logger.error('Login failed for user | password: %s', password);
|
|
353
428
|
```
|
|
354
429
|
|
|
355
|
-
### Log
|
|
430
|
+
### Log levels
|
|
431
|
+
|
|
432
|
+
`ILogger` has exactly five levels, each a direct method. `alert`, `http`, `verbose`, and `silly` don't exist.
|
|
356
433
|
|
|
357
|
-
| Level | Use
|
|
434
|
+
| Level | Use for |
|
|
358
435
|
|-------|---------|
|
|
436
|
+
| `emerg` | The process cannot continue - unrecoverable |
|
|
359
437
|
| `error` | Exceptions that need attention |
|
|
360
438
|
| `warn` | Recoverable issues, deprecation warnings |
|
|
361
439
|
| `info` | Important business events |
|
|
362
|
-
| `debug` | Detailed debugging information |
|
|
440
|
+
| `debug` | Detailed debugging information (gated on `DEBUG`) |
|
|
363
441
|
|
|
364
442
|
```typescript
|
|
365
443
|
// Error - requires attention
|
|
@@ -375,85 +453,95 @@ this.logger.info('[order] Created | orderId: %s | userId: %s', orderId, userId);
|
|
|
375
453
|
this.logger.debug('[query] Executing | sql: %s | params: %j', sql, params);
|
|
376
454
|
```
|
|
377
455
|
|
|
378
|
-
##
|
|
456
|
+
## Handle async errors
|
|
379
457
|
|
|
380
458
|
### Promises
|
|
381
459
|
|
|
460
|
+
`async`/`await` propagates errors on its own - most of the time, you write no error handling at all.
|
|
461
|
+
|
|
382
462
|
```typescript
|
|
383
|
-
//
|
|
384
|
-
async
|
|
463
|
+
// Good - errors propagate naturally with async/await
|
|
464
|
+
const processOrder = async (orderId: string) => {
|
|
385
465
|
const order = await orderRepository.findById({ id: orderId }); // Throws if fails
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
466
|
+
return paymentService.charge(order); // Throws if fails
|
|
467
|
+
};
|
|
389
468
|
|
|
390
|
-
//
|
|
391
|
-
async
|
|
469
|
+
// Good - explicit catch when you need to handle
|
|
470
|
+
const processOrderWithFallback = async (order: TOrder) => {
|
|
392
471
|
try {
|
|
393
472
|
return await paymentService.charge(order);
|
|
394
473
|
} catch (error) {
|
|
395
|
-
this.logger.warn('[processOrder] Primary payment failed, trying backup');
|
|
396
|
-
return
|
|
474
|
+
this.logger.warn('[processOrder] Primary payment failed, trying backup | error: %s', error);
|
|
475
|
+
return backupPaymentService.charge(order);
|
|
397
476
|
}
|
|
398
|
-
}
|
|
477
|
+
};
|
|
399
478
|
|
|
400
|
-
//
|
|
401
|
-
async
|
|
479
|
+
// Bad - swallowing errors
|
|
480
|
+
const processOrderSilently = async () => {
|
|
402
481
|
try {
|
|
403
482
|
await dangerousOperation();
|
|
404
483
|
} catch (error) {
|
|
405
484
|
// Error is swallowed - no one knows it happened!
|
|
406
485
|
}
|
|
407
|
-
}
|
|
486
|
+
};
|
|
408
487
|
```
|
|
409
488
|
|
|
410
|
-
### Fire-and-
|
|
489
|
+
### Fire-and-forget
|
|
490
|
+
|
|
491
|
+
But a call you don't `await` needs its own `.catch()`, or a rejection crashes the process as an unhandled rejection.
|
|
411
492
|
|
|
412
493
|
```typescript
|
|
413
|
-
//
|
|
494
|
+
// Good - log errors from fire-and-forget operations
|
|
414
495
|
this.sendNotification(userId).catch(error => {
|
|
415
|
-
this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error
|
|
496
|
+
this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error);
|
|
416
497
|
});
|
|
417
498
|
|
|
418
|
-
//
|
|
499
|
+
// Good - use void to indicate intentional fire-and-forget
|
|
419
500
|
void this.analytics.track('order_created', { orderId });
|
|
420
501
|
|
|
421
|
-
//
|
|
502
|
+
// Bad - unhandled promise rejection
|
|
422
503
|
this.sendNotification(userId); // If this rejects, crash!
|
|
423
504
|
```
|
|
424
505
|
|
|
425
|
-
##
|
|
506
|
+
## Roll back a transaction on failure
|
|
426
507
|
|
|
427
508
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
509
|
+
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
510
|
+
|
|
511
|
+
const transferFunds = async (opts: { from: string; to: string; amount: number }) => {
|
|
512
|
+
const { from, to, amount } = opts;
|
|
513
|
+
const transaction = await accountRepository.beginTransaction();
|
|
430
514
|
|
|
431
515
|
try {
|
|
432
|
-
await accountRepository.debit({ id: from, amount, options: { transaction
|
|
433
|
-
await accountRepository.credit({ id: to, amount, options: { transaction
|
|
516
|
+
await accountRepository.debit({ id: from, amount, options: { transaction } });
|
|
517
|
+
await accountRepository.credit({ id: to, amount, options: { transaction } });
|
|
434
518
|
|
|
435
|
-
await
|
|
519
|
+
await transaction.commit();
|
|
436
520
|
return { success: true };
|
|
437
521
|
} catch (error) {
|
|
438
|
-
await
|
|
522
|
+
await transaction.rollback();
|
|
439
523
|
|
|
440
524
|
// Re-throw with context
|
|
441
525
|
throw getError({
|
|
442
526
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
443
527
|
message: '[transferFunds] Transaction failed',
|
|
444
|
-
|
|
528
|
+
cause: error,
|
|
529
|
+
extra: { from, to, amount },
|
|
445
530
|
});
|
|
446
531
|
}
|
|
447
|
-
}
|
|
532
|
+
};
|
|
448
533
|
```
|
|
449
534
|
|
|
450
|
-
|
|
535
|
+
> [!NOTE]
|
|
536
|
+
> `rollback()` throws on failure, so it belongs in the `catch` block exactly as shown here - never after a `commit()` you already awaited outside one. A rollback that follows a **failed** commit is a deliberate no-op: the transaction is already torn down, so this canonical shape stays safe.
|
|
537
|
+
|
|
538
|
+
## Parse errors on the client
|
|
451
539
|
|
|
452
|
-
|
|
540
|
+
Branch on `normalized.code` - it's always present. There's no top-level `messageCode`.
|
|
453
541
|
|
|
454
542
|
```typescript
|
|
455
543
|
// TypeScript client example
|
|
456
|
-
async
|
|
544
|
+
const createUser = async (data: TCreateUserRequest): Promise<TUser> => {
|
|
457
545
|
const response = await fetch('/api/users', {
|
|
458
546
|
method: 'POST',
|
|
459
547
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -464,38 +552,44 @@ async function createUser(data: CreateUserRequest): Promise<User> {
|
|
|
464
552
|
const error = await response.json();
|
|
465
553
|
|
|
466
554
|
switch (response.status) {
|
|
467
|
-
case 400:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
555
|
+
case 400: {
|
|
556
|
+
// Context passed to getError(...) arrives under `extra`, not `details`
|
|
557
|
+
throw new ValidationError(error.message, error.extra);
|
|
558
|
+
}
|
|
559
|
+
case 401: {
|
|
471
560
|
window.location.href = '/login';
|
|
472
561
|
throw new AuthError('Please log in');
|
|
473
|
-
|
|
562
|
+
}
|
|
563
|
+
case 404: {
|
|
474
564
|
throw new NotFoundError(error.message);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
565
|
+
}
|
|
566
|
+
case 422: {
|
|
567
|
+
// Field-level issues live in details.cause
|
|
568
|
+
const fieldErrors = error.details?.cause?.reduce((accumulator, issue) => {
|
|
569
|
+
accumulator[issue.path] = issue.message;
|
|
570
|
+
return accumulator;
|
|
480
571
|
}, {});
|
|
481
|
-
throw new ValidationError(
|
|
482
|
-
|
|
572
|
+
throw new ValidationError(error.normalized.code, fieldErrors);
|
|
573
|
+
}
|
|
574
|
+
case 429: {
|
|
483
575
|
throw new RateLimitError('Too many requests. Try again later.');
|
|
484
|
-
|
|
576
|
+
}
|
|
577
|
+
default: {
|
|
485
578
|
throw new ApiError(error.message || 'Something went wrong');
|
|
579
|
+
}
|
|
486
580
|
}
|
|
487
581
|
}
|
|
488
582
|
|
|
489
583
|
return response.json();
|
|
490
|
-
}
|
|
584
|
+
};
|
|
491
585
|
```
|
|
492
586
|
|
|
493
|
-
##
|
|
587
|
+
## Checklist
|
|
494
588
|
|
|
495
589
|
| Category | Check |
|
|
496
590
|
|----------|-------|
|
|
497
591
|
| **Services** | Business rule violations throw appropriate errors |
|
|
498
|
-
| **Repositories** |
|
|
592
|
+
| **Repositories** | Constraint errors left to the global handler; only custom messages wrapped |
|
|
499
593
|
| **Controllers** | Errors propagate to global handler |
|
|
500
594
|
| **Async** | All promises have error handling |
|
|
501
595
|
| **Transactions** | Always rollback on error |
|
|
@@ -503,7 +597,7 @@ async function createUser(data: CreateUserRequest): Promise<User> {
|
|
|
503
597
|
| **Responses** | Consistent error format returned |
|
|
504
598
|
| **Security** | No sensitive data in error messages |
|
|
505
599
|
|
|
506
|
-
## See
|
|
600
|
+
## See also
|
|
507
601
|
|
|
508
602
|
- [Common Pitfalls](./common-pitfalls) - Error handling mistakes
|
|
509
603
|
- [Testing Strategies](./testing-strategies) - Testing error scenarios
|