Problem
Error-to-message extraction exists three times with three different behaviors:
handleApiError in src/services/outages.ts — handles detail (string) and message.
extractErrorMessage in src/services/sla.ts — reads message only (broken for FastAPI — companion issue).
extractErrorMessage in src/services/bulkImportService.ts — reads message only.
normalizeApiError in src/lib/errors.ts — reads detail (string or array), message, correlation ids — a fourth implementation for the axios path.
Consequences:
- Behavior varies by service: an outages failure shows the server reason; an SLA failure shows a generic fallback; the axios interceptor normalizes yet another way — the same backend error renders differently depending on which service threw it.
- Fixes are duplicated: any improvement (detail-array handling, field-level mapping, correlation ids) must be re-implemented per copy or silently missed.
- The duplication is invisible: each helper is private to its module, so there is no single place to audit "how do we render API errors".
Root cause
The error helpers were written per-service before a shared convention existed.
Why this is architecturally hard
- The consolidation target is a single
extractApiErrorMessage (string/array detail, message, fallback) that normalizeApiError also delegates to — but the axios interceptor's version returns a structured NormalizedApiError (kind, correlationId) while the service helpers return plain strings; unifying the return shape is a type decision.
- Service callers use the string for UI copy; changing the shared helper's output must keep every call site's rendering working (some concatenate the message into longer strings).
- Tests exist per service (bulk-import-view, payments-view, sla-related); the consolidation must keep their assertions meaningful by testing the shared helper once plus per-service fallback text.
Proposed design
Extract a shared error helper (and, for the axios path, make normalizeApiError use it), keep per-service fallback text as parameters, and move the error-mapping tests to the shared module with per-service spot checks.
Acceptance criteria
Service
Tests
Out of scope
Field-level 422 mapping (tracked separately).
Getting started
Good first files to read: src/services/outages.ts, src/services/sla.ts, src/services/bulkImportService.ts, src/lib/errors.ts.
Problem
Error-to-message extraction exists three times with three different behaviors:
handleApiErrorinsrc/services/outages.ts— handlesdetail(string) andmessage.extractErrorMessageinsrc/services/sla.ts— readsmessageonly (broken for FastAPI — companion issue).extractErrorMessageinsrc/services/bulkImportService.ts— readsmessageonly.normalizeApiErrorinsrc/lib/errors.ts— readsdetail(string or array),message, correlation ids — a fourth implementation for the axios path.Consequences:
Root cause
The error helpers were written per-service before a shared convention existed.
Why this is architecturally hard
extractApiErrorMessage(string/arraydetail,message, fallback) thatnormalizeApiErroralso delegates to — but the axios interceptor's version returns a structuredNormalizedApiError(kind, correlationId) while the service helpers return plain strings; unifying the return shape is a type decision.Proposed design
Extract a shared error helper (and, for the axios path, make
normalizeApiErroruse it), keep per-service fallback text as parameters, and move the error-mapping tests to the shared module with per-service spot checks.Acceptance criteria
Service
Tests
Out of scope
Field-level 422 mapping (tracked separately).
Getting started
npm testGood first files to read:
src/services/outages.ts,src/services/sla.ts,src/services/bulkImportService.ts,src/lib/errors.ts.