feat(result): typed, translatable error handling (@carbon/result)#892
Open
sidgaikwad wants to merge 7 commits into
Open
feat(result): typed, translatable error handling (@carbon/result)#892sidgaikwad wants to merge 7 commits into
sidgaikwad wants to merge 7 commits into
Conversation
Service functions now return a typed Result<T, E> whose failure side is a tagged error carrying a Lingui message descriptor. Errors are translated at the action boundary into the requester's locale, so error toasts are no longer hardcoded English and raw Postgres errors never reach users. - Add @carbon/result: re-exports better-result; a Lingui-aware CarbonError base (class-level default + call-site override, serializable values) and the six core errors (NotFound, Validation, Conflict, BusinessRule, Database, ExternalService). - Rename the @carbon/auth Result type to FlashResult, freeing the name "Result" for the typed Result everywhere. - Add @carbon/database adapters: fromQuery (PostgrestError -> DatabaseError, no-rows -> NotFoundError with entity context) and fromTransaction (thrown exception -> DatabaseError, raw error preserved for logging). - Add errorFlash/successFlash boundary converters in @carbon/auth plus a per-request getRequestI18n helper in the erp and mes apps; translation happens at write time in the requester's locale. - Convert the approvals module (approve/reject/cancel) as the reference pilot; update the purchase-order, supplier and quality-document routes and make the MCP direct-executor Result-aware. - Register packages/result/src in both Lingui catalogs and document the Greenfield Rule in AGENTS.md. Tests cover the core errors, the database adapters, the boundary converter and the approvals pilot. Run `pnpm --filter @carbon/result demo` to see every error and its translations side by side.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…anslate error catalogs
- Merge the boundary converters into the existing `error()` / `success()`
helpers via overloads: pass a tagged `TranslatableError` (or descriptor) plus
the request i18n to translate at write time. The legacy `(value, string)`
overload is unchanged, so the existing flash call sites are untouched. i18n is
optional — without it the English source is used, with `{placeholder}`s still
interpolated.
- Update the approvals pilot routes to call `error(result.error, i18n)`.
- Translate the six core + two approvals error messages across all 11 locales.
- Remove the throwaway result-demo route.
…bon-result-error-handling
…mport getPreferenceHeaders from source - cancelApprovalRequest: pull each Supabase query into a named const, then `fromQuery(query, ctx)`. Reads in natural order (build the query, then convert) instead of wrapping a long fluent chain inside a call. The named const also lets TypeScript infer the row type, so the explicit generics are dropped. - Import `getPreferenceHeaders` from `@carbon/utils` (its real source) instead of `@carbon/react`, where it only resolved via a fragile transitive re-export. - Tighten the error-message translations: consistent formal register across all locales, and gender-safe phrasing for the not-found message (verb/infinitive forms rather than gendered adjectives).
…bon-result-error-handling # Conflicts: # apps/erp/app/routes/api+/mcp+/lib/tool-metadata.json # packages/locale/locales/de/erp.po # packages/locale/locales/en/erp.po # packages/locale/locales/es/erp.po # packages/locale/locales/fr/erp.po # packages/locale/locales/hi/erp.po # packages/locale/locales/it/erp.po # packages/locale/locales/ja/erp.po # packages/locale/locales/pl/erp.po # packages/locale/locales/pt/erp.po # packages/locale/locales/ru/erp.po # packages/locale/locales/tr/erp.po # packages/locale/locales/zh/erp.po
…dex access - Update the demo's inline es/de catalogs and assertions to match the production translations (gender-safe not-found, formal register). - Use `rows.find(...)` instead of array destructuring so the test type-checks under `noUncheckedIndexedAccess`.
The merge auto-resolved pnpm-lock.yaml into an inconsistent state (a dangling `semver@7.7.3` reference with no entry), so `pnpm install --frozen-lockfile` failed in CI. Regenerated the lockfile so it matches the merged package.json files; `--frozen-lockfile` now passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Service operations used to fail in an untyped way: services returned raw Supabase
{ data, error }or threw genericErrors, and the user-facing message was hardcoded English authored at the route. Despite 11 supported locales, every error toast was English — and several services passed the rawPostgrestErrorstraight through, so a rawduplicate key value violates unique constraint …could land in a user's toast.This introduces
@carbon/result: service functions return a typedResult<T, E>whose failure side is a tagged error carrying a Lingui message descriptor. The error is translated at the action boundary into the requester's locale and shown through the existing flash/toast machinery. Developers get exhaustive, typed failure handling; users get messages in their own language; raw database errors never reach them.Design is recorded in
docs/adr/0001, the PRD indocs/prd, and the glossary inCONTEXT.md.Architecture
The error is translated once, at write time, in the requester's locale — not at creation (no presentation concerns in service signatures) and not on the client.
What's in the box
@carbon/result(new)better-result; a Lingui-awareCarbonErrorbase (class-level default + call-site override, serializable interpolation values, yieldable inResult.gen) and the six closed-set core errors:NotFoundError,ValidationError,ConflictError,BusinessRuleError,DatabaseError,ExternalServiceError.@carbon/authResulttype →FlashResult(frees the name for the typed Result). AddederrorFlash/successFlashboundary converters that resolve the descriptor via the request i18n and log the raw cause for on-call debugging.@carbon/databasefromQuery(query, { entity, id })mapsPostgrestError→DatabaseErrorand no-rows →NotFoundError;fromTransaction(db, fn)maps thrown exceptions →DatabaseError(raw error preserved). Keeps@carbon/resultfree of Supabase/Kysely types.erp/mesgetRequestI18n(request)builds a request-scoped i18n for the boundary.approveRequest/rejectRequest/cancelApprovalRequestreturnResult; the purchase-order, supplier and quality-document routesmatch → errorFlash; the MCPdirect-executoris now Result-aware so a typed failure isn't reported as success.packages/result/srcadded to both Lingui catalogs (the pre-commit hook extracted the new ids). Greenfield Rule documented inAGENTS.md.Conflict vs. BusinessRule
Per the glossary: Conflict = the operation is valid but the current state already blocks/satisfies it (e.g. "already clocked in"); BusinessRule = the operation itself violates an invariant (e.g. "insufficient quantity"). There is no
PermissionError— auth and RLS enforce permissions upstream.See it yourself
Prints every error and its translations (English = expected, plus Spanish & German) resolved through the exact
errorFlashpath. Expected output and manual in-app verification steps are indocs/result-error-demo.md.Tests
packages/result/src/errors.test.tsResult.genpackages/database/src/result.test.tspackages/auth/src/utils/result.test.tsapps/erp/.../shared.service.test.tspackages/result/src/demo.test.ts35 new tests;
@carbon/result,@carbon/database,erp,jobs,mestypecheck clean.Adoption (Greenfield Rule)
New and materially-modified service functions return
Result; untouched legacy code keeps its current style. No big-bang rewrite. The approvals module is the reference example.Follow-ups (out of scope here)
pnpm translateto LLM-translate the newly-extracted message ids for all 11 locales (this PR extracts them; the strings ship untranslated, falling back to English).Resulttype still exists inapps/erp/app/types/index.ts(aFlashResult-shaped sibling); renaming it for full unambiguity is a separate mechanical change.