@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.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,52 +1,106 @@
1
1
  # Error Handling
2
2
 
3
- Comprehensive guide to handling errors gracefully in IGNIS applications.
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
- ## Error Handling Philosophy
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
- | Principle | Description |
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
- ## 1. Using `getError` Helper
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
- IGNIS provides `getError` for creating consistent, structured errors.
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
- // Basic error
36
+ // Error with context - any key the framework does not model lands in `extra`
22
37
  throw getError({
23
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
24
- message: 'User not found',
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
- // Error with details
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
- // Error with context (for logging)
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
- details: { userId: requestedId },
55
+ cause: error,
56
+ extra: { userId: requestedId },
42
57
  });
43
58
  ```
44
59
 
45
- ## 2. HTTP Status Code Reference
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
- Use the correct status code for each error type:
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
- | Code | Constant | Use When |
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 Database Error Handling
63
- Database errors in SQLSTATE classes `22` (data exception), `23` (integrity constraint - unique, foreign key, not null, check, exclusion), and `44` (WITH CHECK OPTION violation) are automatically converted to HTTP 400 by the global error middleware. Transient conflicts (`40001` serialization failure, `40P01` deadlock) become HTTP 409 with a retryable message. You don't need to catch these manually. Other classes (e.g. syntax / undefined column) stay 500, and production responses are sanitized - see [Repository Layer Errors](#repository-layer-errors).
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
- ## 3. Error Handling Patterns
120
+ ## Handle errors by layer
121
+
122
+ ### Service layer
67
123
 
68
- ### Service Layer Errors
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.message);
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
- ### Controller Layer Errors
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
- Controllers should delegate to services and let the global error handler catch exceptions:
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 Layer Errors
210
+ ### Repository layer
153
211
 
154
- Database errors in SQLSTATE classes `22` (data exception), `23` (integrity constraint - unique, foreign key, not null, check, exclusion), and `44` (WITH CHECK OPTION violation) are **automatically handled** by the global error middleware and return HTTP 400. Transient conflicts (`40001` serialization failure, `40P01` deadlock) return HTTP 409 with a generic retryable message. Codes outside those classes (e.g. class `42` undefined column - an application/SQL bug) correctly stay 500.
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
- "requestId": "abc123"
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** - `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` + server logs to diagnose.
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
- { "message": "Unique constraint violation", "messageCode": "core.system_error", "statusCode": 400, "requestId": "abc123" }
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
- You don't need to wrap repository calls in try-catch for constraint errors. If you need custom error messages, you can still handle them explicitly:
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<TCreateResult<TUser>> {
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
- ## 4. Global Error Handler
272
+ ## Customize the global handler
200
273
 
201
- IGNIS includes a built-in error handler. Customize behavior in your application:
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 { ApplicationError, MessageCode } from '@venizia/ignis-helpers';
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.onError((error, c) => {
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.message);
290
+ this.logger.error('[%s] Error | %s', requestId, error);
217
291
 
218
- // Handle known application errors
219
- if (error instanceof ApplicationError) {
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
- messageCode: error.messageCode, // already lower-cased, never undefined
224
- details: error.details,
298
+ normalized: error.normalized, // { text, code, args } - never undefined
299
+ extra: error.extra,
225
300
  requestId,
226
- }, error.statusCode as StatusCode);
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
- messageCode: MessageCode.DEFAULT,
235
- details: { cause: error.errors },
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
- messageCode: MessageCode.DEFAULT,
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
- ## 5. Error Response Format
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
- messageCode?: string; // stable, localizable code (validation: from params.code or the raw Zod code)
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
- cause?: Array<{
265
- path: string;
266
- message: string;
267
- code: string;
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
- **Example Responses:**
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
- "messageCode": "core.system_error",
282
- "requestId": "abc123"
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
- // 404 Not Found
286
- // Extra keys passed to getError(...) (e.g. `details`) surface under `extra`;
287
- // the top-level `details` object is reserved for middleware context (url, path, stack, cause).
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
- "messageCode": "core.system_error",
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
- // 422 Validation Error
297
- // `message`/`messageCode` come from the first failing issue - its `params.code` if the schema set
298
- // one, otherwise the raw Zod code (e.g. `invalid_type`, `too_small`). The full list stays in `details.cause`.
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
- "messageCode": "user.email.invalid",
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
- ## 6. Logging Errors
399
+ ## Log errors
325
400
 
326
- ### `%s`, Never `%j`, for an `Error`
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 `message` and `stack`, logging little more than `{}`. Always use `%s` to log an `Error` instance; reserve `%j`/`%o` for plain data objects.
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
- // Good - %s prints message + stack
406
+ // Good - %s prints message + stack
332
407
  this.logger.error('[createOrder] Failed | error: %s', error);
333
408
 
334
- // Bad - %j drops message and stack (non-enumerable)
409
+ // Bad - %j drops the stack
335
410
  this.logger.error('[createOrder] Failed | error: %j', error);
336
411
  ```
337
412
 
338
- ### What to Log
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
- // Good - Context for debugging
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.message);
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
- // Bad - No context
423
+ // Bad - no context, and `.message` throws the stack away
349
424
  this.logger.error(error.message);
350
425
 
351
- // Bad - Sensitive data
426
+ // Bad - sensitive data
352
427
  this.logger.error('Login failed for user | password: %s', password);
353
428
  ```
354
429
 
355
- ### Log Levels
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 For |
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
- ## 7. Async Error Handling
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
- // Good - Errors propagate naturally with async/await
384
- async function processOrder(orderId: string) {
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
- const payment = await paymentService.charge(order); // Throws if fails
387
- return payment;
388
- }
466
+ return paymentService.charge(order); // Throws if fails
467
+ };
389
468
 
390
- // Good - Explicit catch when you need to handle
391
- async function processOrderWithFallback(orderId: string) {
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 await backupPaymentService.charge(order);
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
- // Bad - Swallowing errors
401
- async function processOrder(orderId: string) {
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-Forget with Error Handling
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
- // Good - Log errors from fire-and-forget operations
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.message);
496
+ this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error);
416
497
  });
417
498
 
418
- // Good - Use void to indicate intentional fire-and-forget
499
+ // Good - use void to indicate intentional fire-and-forget
419
500
  void this.analytics.track('order_created', { orderId });
420
501
 
421
- // Bad - Unhandled promise rejection
502
+ // Bad - unhandled promise rejection
422
503
  this.sendNotification(userId); // If this rejects, crash!
423
504
  ```
424
505
 
425
- ## 8. Transaction Error Handling
506
+ ## Roll back a transaction on failure
426
507
 
427
508
  ```typescript
428
- async function transferFunds(from: string, to: string, amount: number) {
429
- const tx = await accountRepository.beginTransaction();
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: tx } });
433
- await accountRepository.credit({ id: to, amount, options: { transaction: tx } });
516
+ await accountRepository.debit({ id: from, amount, options: { transaction } });
517
+ await accountRepository.credit({ id: to, amount, options: { transaction } });
434
518
 
435
- await tx.commit();
519
+ await transaction.commit();
436
520
  return { success: true };
437
521
  } catch (error) {
438
- await tx.rollback();
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
- details: { from, to, amount, originalError: error.message },
528
+ cause: error,
529
+ extra: { from, to, amount },
445
530
  });
446
531
  }
447
- }
532
+ };
448
533
  ```
449
534
 
450
- ## 9. Client-Side Error Handling
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
- Guide for API consumers:
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 function createUser(data: CreateUserRequest): Promise<User> {
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
- throw new ValidationError(error.message, error.details);
469
- case 401:
470
- // Redirect to login
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
- case 404:
562
+ }
563
+ case 404: {
474
564
  throw new NotFoundError(error.message);
475
- case 422:
476
- // Handle field-level errors
477
- const fieldErrors = error.details?.cause?.reduce((acc, e) => {
478
- acc[e.path] = e.message;
479
- return acc;
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('Validation failed', fieldErrors);
482
- case 429:
572
+ throw new ValidationError(error.normalized.code, fieldErrors);
573
+ }
574
+ case 429: {
483
575
  throw new RateLimitError('Too many requests. Try again later.');
484
- default:
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
- ## Error Handling Checklist
587
+ ## Checklist
494
588
 
495
589
  | Category | Check |
496
590
  |----------|-------|
497
591
  | **Services** | Business rule violations throw appropriate errors |
498
- | **Repositories** | Database errors are caught and wrapped |
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 Also
600
+ ## See also
507
601
 
508
602
  - [Common Pitfalls](./common-pitfalls) - Error handling mistakes
509
603
  - [Testing Strategies](./testing-strategies) - Testing error scenarios