Changelog - 2026-07-16
Error Handling
Bug Fix Enhancement New APIIn one line. Every error now carries a normalized message ready for translation. You can declare reusable error definitions in a catalog. cause finally reaches Error.cause.
The problem it solves
A client rendering an error had to pair messageCode and extra.messageArgs by hand to get a translatable code and its arguments. cause fared worse: it was swept into extra.cause, while the native Error.cause field stayed undefined. The wrapped failure's stack never surfaced. normalized and a working cause fix both.
What changed
- Every error now carries a
normalizedmessage. One object ({ text, code, args }) is all a client needs to render and translate any error. All five handler branches emit it, including validation. causenow actually reachesError.cause. The error handler reads the native field. Five framework call sites - including boot failures - were losing theircausetoextra.causeinstead.- New: declare a domain error once, reuse it everywhere. A catalog entry fixes an error's code, HTTP status, and default message so every call site that raises it stays in sync. See Details below.
- New:
transformbuildsnormalizedyourself.normalized.textcan be a rendered, translated string whilemessagekeeps the raw text the throw site wrote. - The error layer moved to
@venizia/ignis-inversion. Backend imports are unchanged - keep importing from@venizia/ignis-helpers. - Browser applications already depend on inversion for dependency injection. They can now raise and read the same errors the server does.
- This also deletes a second, divergent
ApplicationErrorthat lived in inversion and never resolved itsmessageCode.
Who is affected
- Anyone reading
error.messageCodeorerror.extra.messageArgson the client. Both still work but are deprecated. Migrate toerror.normalized.code/error.normalized.args. - Browser or frontend applications. Can now import
ApplicationError,getError, and related types from@venizia/ignis-inversiondirectly. - Anyone spreading an error definition into
getError. Not a regression - it was already broken - but worth fixing now. See Details. - Everyone else. No action needed.
getErroraccepts every shape it accepted before. Unknown keys still land inextra, and the wire only gainsnormalized.
No breaking changes
getError's input is unchanged. Any key it does not model still rides into extra:
typescript
throw getError({ message: 'Active transaction exists', transaction: { id, uid, status } });
// -> error.extra.transaction, exactly as beforeThe wire shape only gains normalized. Nothing is removed, nothing moves.
Details
messageCodeandextra.messageArgsare deprecated in favor ofnormalized.code/normalized.args. They stay only until every client has migrated.- Never spread a definition into
getError.throw getError({ ...SomeErrors.SOME_ERROR })reads naturally and is wrong. - A definition carries
key, notmessageCode. The key lands inextra.keyinstead, and the error degrades tocore.system_error. - That is unlocalizable, since clients branch on the code. The status and message still arrive, so it survives review anyway.
- Pass it as
errorinstead. Nothing catches this for you: the index signature that carries your context acceptskeytoo. - The index signature is a deliberate trade: it is what lets a throw site attach context the framework knows nothing about.
- That is also why
getError({ message, statuscode: 503 })compiles, leavesstatusCodeat400, and parks503inextra.statuscode. The framework cannot tell your context from your typo. - The catalog entry's
descriptionfield is not a code comment. It is the only context a translator gets, since they work from a spreadsheet, not the source. - Declare a catalog entry's
keyas a string literal, notMessageCode.build(...).build()returnsstring, which widens the key registry toRecord<string, true>and silently breaks autocomplete. - Free-form
getError({ message })is unchanged and still the right choice for errors nobody translates (invariants, misconfiguration, seed guards). - Inversion ships both a CJS and an ESM build, so
instanceof ApplicationErrorcan still fail across a package boundary even after this change. Keep usingisApplicationError(). categoryon a catalog entry is metadata for grouping and translator exports; it does not reach the response.
See Also
- Error Helper - the full reference
- Error Handling - when to catalog and when to raise free-form