Error handling
The layers
┌─────────────────────────────────────────────────────────────────────┐
│ window.onerror (main.ts) │
│ uncaught JS errors → Segment 'WINDOW ERROR' track, returns false │
├─────────────────────────────────────────────────────────────────────┤
│ ErrorHandler → AppErrorHandler │
│ Angular's global handler. Dev: log + rethrow. Prod: log only. │
├─────────────────────────────────────────────────────────────────────┤
│ PendingInterceptorService │
│ toasts any response with status >= 500, then rethrows │
├─────────────────────────────────────────────────────────────────────┤
│ AuthInterceptor │
│ 401 with a token → dispatch Logout with a message, then rethrow │
├─────────────────────────────────────────────────────────────────────┤
│ Service-level catchError │
│ toast + rethrow, or map to an _ERROR store action │
├─────────────────────────────────────────────────────────────────────┤
│ ErrorParserService → ErrorVisualizationService │
│ turn an error object into a message, show it as a toast │
├─────────────────────────────────────────────────────────────────────┤
│ Logger / ErrorLogger │
│ dev-only console output; Segment events with a redacted store dump │
├─────────────────────────────────────────────────────────────────────┤
│ FetchedRequestsInterceptor │
│ ring buffer of the last 50 requests in the `errors` state slice │
└─────────────────────────────────────────────────────────────────────┘
|
A 5xx response typically produces two toasts: one from
|
AppErrorHandler
src/app/shared/error-handler.ts, bound to Angular’s ErrorHandler token:
handleError(error: any) {
if (isDevMode()) {
this.displayConsoleMessage(error, true); // grouped console.error
throw error; // rethrow in dev
} else {
const displayErrors = this.appConfig.env.displayErrorsInConsole;
const showErrorPopup = this.appConfig.env.displayErrorsPopup;
this.handleErrorProd(error, displayErrors, showErrorPopup);
}
}
Two environment switches govern production behaviour:
| Key | Effect |
|---|---|
|
|
|
|
|
|
ErrorParserService
shared/services/error-parser/error-parser.service.ts turns an
ErrorInterface ({ name, errorObject }) into a display string. The precedence, in order:
-
Empty name and empty error object →
'Unknown Error.'. -
errorObjectis a string →'Error:' + name + ' : ' + errorObject. -
errorObject.error.meta.errors[]exists → the internal API’s multi-error envelope; concatenates everymessageseparated by', '. -
errorObject.error.messageexists → returns just that message, without the prefix. -
errorObject.messageexists →'Error:' + name + ' : ' + message. -
Otherwise →
'Error:' + name + ' : Error unrecognized.'.
Constants live in StandardErrorMessages:
| Constant | Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
GENERAL_ERROR_MESSAGE (shared/consts/general-error-message.ts) —
'Sorry, something went wrong…' — is the user-facing fallback used by components that do
not parse.
|
Branch 4 returns the bare message with no |
The multi-error envelope this understands is the internal API’s:
{ "error": { "meta": { "errors": [ { "message": "…" }, { "message": "…" } ] } } }
See data modelling for the other response envelopes.
ErrorVisualizationService
The single presentation path:
readonly ERROR_SHOW_TIMEOUT = 9000;
showError(errorData: ErrorInterface): void {
this.toastrService.error(this.errorParserService.parse(errorData), null,
{ timeOut: this.ERROR_SHOW_TIMEOUT });
}
Errors show for 9 seconds. Both this service and ErrorParserService have .mock.service
siblings, and shared/services/toastr-mock/toastr.mock.service.ts stubs Toastr for specs.
|
For a user-facing error, inject |
Service-level patterns
Three coexist, oldest first:
| Pattern | Where |
|---|---|
|
Older services ( |
|
Repository services. |
|
Store-backed flows. The component reads the error out of state
( |
CardManagementService uses the modern throwError(() ⇒ err) factory plus a
console.error with structured context — the pattern to copy for new services.
Logging
Logger
src/app/shared/logger.ts — static, dev-only:
Logger.log(msg, ...params) // grouped, collapsed: '------- SILENT LOG DEBUG --------'
Logger.error(msg, ...params) // grouped: '!!!!!!! SILENT LOG ERROR !!!!!!!'
Logger.ex(exception, ...params) // same as error
All three are gated on isDevMode(), so they are silent in production builds. Used
throughout AuthEffects, SupplierEffects and SseService.
It is declared @Injectable() with an AppConfigService constructor parameter but every
method is static, so the injection is never used. It is imported both as
Logger (named) and as the default export.
ErrorLogger
shared/services/error-logger/error-logger.service.ts sends structured events to Segment
with a redacted store dump:
log(title: string, data: any, withStoreDump = true): void {
if (withStoreDump) {
this.store$.pipe(take(1)).subscribe((dump) => {
const store = this.prepareDump(dump);
this.fullstoryEvent(title, { data });
this.fullstoryEvent(title, { store });
});
} else {
this.fullstoryEvent(title, { data });
}
}
private prepareDump(storeData: any): any {
const data = { ...storeData };
delete data['auth']; // ← token and user
delete data['backgroundReports'];
delete data['calc'];
delete data['crm'];
delete data['layout'];
delete data['utils'];
return data;
}
private fullstoryEvent(title, data) {
try { (<any>window).analytics.track(title, data); } catch (err) {}
}
|
Before adding an |
The method is named fullstoryEvent but calls window.analytics.track — Segment, not
FullStory. Historical naming.
Request ring buffer
FetchedRequestsInterceptor dispatches FetchRequest(req) for every request; the
errors reducer keeps the last 50, JSON.stringify-ed, in state
(REQUESTS_AMOUNT = 50).
function cachedRequestArrayHandler(array, incomingRequest) {
let newArr = array;
if (array.length === REQUESTS_AMOUNT) { newArr = array.slice(0, -1); }
newArr.unshift(JSON.stringify(incomingRequest, null, 1));
return newArr;
}
Note newArr is the same array when under the cap, so unshift mutates the existing
state array. It is a debugging aid, visible in Redux DevTools and included in
`ErrorLogger’s store dump.
The error page
shared/error-handling/tetris/ — a playable Tetris implementation
(game.service.ts, constants.ts, board/, piece/, with specs) shown on the error
page. shared/page-not-found/ handles 404s, and FeatureFlagGuard navigates to /404
when a flag is off.
Toast conventions
Toasts are the only notification channel — errors never use dialogs.
| Source | Wording |
|---|---|
|
Parsed message, 9 s. |
|
|
|
|
|
|
|
|
Repository services |
Feature-specific "Cannot …" messages. |
Store error state
Errors that must survive a render are kept in state rather than toasted:
| Slice field | Set by |
|---|---|
|
|
|
|
|
The corresponding |
|
|
|
|
|
|
The pattern is consistent: an error written into state must be cleared explicitly by the component that displayed it.
Guidance
-
User-facing failure →
ErrorVisualizationService.showError({ name, errorObject }). -
Failure that must persist across renders → an
_ERRORaction, and remember to clear it. -
Diagnostic detail →
Logger.log/Logger.error(dev-only). -
Never
catchErrorinto a silentof(null)for anything the user initiated. -
Do not add a second toast for an error the interceptor already toasts.
-
Do not send store dumps to analytics without checking what the redaction leaves behind.
Gotchas summary
| Issue | Detail |
|---|---|
Duplicate toasts on 5xx |
Interceptor plus caller. |
|
Read from config, never acted on. |
Sentry is not wired up |
Configured everywhere, used nowhere. |
Parser prefix is inconsistent |
One branch omits |
|
Redaction keeps |
|
Constructor injection unused. |
Ring buffer mutates state |
|