Skip to content

Changelog - 2026-07-16 ​

Error Handling ​

Bug Fix Enhancement New API

In 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 normalized message. One object ({ text, code, args }) is all a client needs to render and translate any error. All five handler branches emit it, including validation.
  • cause now actually reaches Error.cause. The error handler reads the native field. Five framework call sites - including boot failures - were losing their cause to extra.cause instead.
  • 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: transform builds normalized yourself. normalized.text can be a rendered, translated string while message keeps 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 ApplicationError that lived in inversion and never resolved its messageCode.

Who is affected ​

  • Anyone reading error.messageCode or error.extra.messageArgs on the client. Both still work but are deprecated. Migrate to error.normalized.code / error.normalized.args.
  • Browser or frontend applications. Can now import ApplicationError, getError, and related types from @venizia/ignis-inversion directly.
  • 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. getError accepts every shape it accepted before. Unknown keys still land in extra, and the wire only gains normalized.

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 before

The wire shape only gains normalized. Nothing is removed, nothing moves.

Details ​

  • messageCode and extra.messageArgs are deprecated in favor of normalized.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, not messageCode. The key lands in extra.key instead, and the error degrades to core.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 error instead. Nothing catches this for you: the index signature that carries your context accepts key too.
  • 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, leaves statusCode at 400, and parks 503 in extra.statuscode. The framework cannot tell your context from your typo.
  • The catalog entry's description field 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 key as a string literal, not MessageCode.build(...). build() returns string, which widens the key registry to Record<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 ApplicationError can still fail across a package boundary even after this change. Keep using isApplicationError().
  • category on a catalog entry is metadata for grouping and translator exports; it does not reach the response.

See Also ​