Application structure
Error Handling
The difference between errors that are thrown, reported and written as warnings; MotifError, errorHandler, setUnexpectedErrorHandler and the safeCall helpers.
MotifJS does not swallow errors coming from user code, but it does not bring the application down either. Every situation the framework throws, reports or writes as a warning has an MJX code, and these situations go down one of three paths. This section covers those paths, the places to listen for errors, and the framework's own error-safe call helpers. The full list of codes is in the Error and Warning Codes table.
Three paths
| Path | When | Where it goes |
|---|---|---|
| Throw | Misuse the caller has to fix right away: the named route does not exist (MJX302), the service is not registered (MJX401), a second ApplicationBuilder (MJX405), Query.first() on an empty array (MJX601) … |
A MotifError is thrown; catch it with try/catch. |
| Report | A user code error that should not stop the flow: a component hook (MJX122), an event handler (MJX123), an effect/binding (MJX208), a route hook and guard (MJX306), disposal and data loading errors … |
errorHandler.report(MotifError): to the console with console.error (app.useLogging(false) turns it off), to the errorHandler.addListener(fn) listeners, and to the devtools bus if there is one. The original error is in cause. Works in production too. |
| Warning | A situation that keeps running but is most likely unwanted: a duplicate key (MJX202), an effect that triggers itself (MJX203), route definition checks (MJX310–MJX316) … |
Only in development mode (app.useDevelopment(true)): console.warn and the devtools warning list. It does not go to listeners. |
MotifError
Every error the framework throws or reports is a MotifError:
import { MotifError } from "@motifx/core";
try {
await app.navigateByName("note", { id: note.id });
} catch (e) {
if (e instanceof MotifError && e.code === 'MJX302') { /* named route not found */ }
}
import { MotifError } from "@motifx/core";
try {
await app.navigateByName("note", { id: note.id });
} catch (e) {
if (e instanceof MotifError && e.code === 'MJX302') { /* named route not found */ }
}
| Field | Description |
|---|---|
code |
MotifErrorCode (such as 'MJX302'). Branch on this, not on the message text; the texts are in English and may change. |
message |
An English message of the form [motifjs] MJX302: …. |
cause |
For reported errors, the original error (the value the user code threw). |
name |
'MotifError' ('TimeoutError' for a Resilience timeout). |
The MotifErrorCode type is the union of all codes; it gives completion when you write switch (e.code).
Listening for reports — errorHandler
import { errorHandler } from "@motifx/core";
const off = errorHandler.addListener((error) => {
navigator.sendBeacon("/api/errors", JSON.stringify({ code: error.code, message: error.message, cause: String(error.cause) }));
});
import { errorHandler } from "@motifx/core";
const off = errorHandler.addListener((error) => {
navigator.sendBeacon("/api/errors", JSON.stringify({ code: error.code, message: error.message, cause: String(error.cause) }));
});
- A listener receives every reported
MotifError(MJX122,MJX123,MJX208,MJX306…). Errors without a code that go tosetUnexpectedErrorHandler(below) reach the listeners as they are too. - Thrown errors do not go to the listener (they already return to the caller), and neither do warnings.
- A listener is not tied to any component; it is removed with the function returned by
addListener(off()). - Listeners are called in the order they were added. An error thrown by one listener does not stop the other listeners or the flow that made the report; the error is rethrown with
setTimeout(…, 0)and shows up in the browser at thewindow.onerrorlevel.safeCallstill returns the fallback value in that case. - You can put your own error on the same path with
errorHandler.report(err);errmust be anError, it does not have to be aMotifError.
The other members of the ErrorHandler class: setDevelopmentMode(b) / isDevelopment() (app.useDevelopment uses these), setConsoleLogging(b) (app.useLogging), reportSuppressed(context, err) (writes to the console only in development mode and does not go to listeners; the framework uses it for errors it swallows internally), getUnexpectedErrorHandler().
Unexpected errors — setUnexpectedErrorHandler
Errors caught inside safeCall/safeCallAsync and errors thrown by Emitter listeners have no code; they go down the errorHandler.onUnexpectedError(err) path:
- The handler given with
setUnexpectedErrorHandler(fn)is called. The default handler rethrows the error withsetTimeout(…, 0): it shows up in the browser at thewindow.onerror/unhandledrejectionlevel, and the calling flow is not interrupted. - Then the
addListenerlisteners receive the error as it is.
import { setUnexpectedErrorHandler } from "@motifx/core";
setUnexpectedErrorHandler((error) => navigator.sendBeacon("/api/errors", String(error))); // send it to the server instead of rethrowing
import { setUnexpectedErrorHandler } from "@motifx/core";
setUnexpectedErrorHandler((error) => navigator.sendBeacon("/api/errors", String(error))); // send it to the server instead of rethrowing
Reports with a code (MJX122 …) do not go to this handler; use addListener for them.
Error-safe call helpers
The helpers the framework uses to wrap its own code are public:
| Function | Behaviour |
|---|---|
safeCall(fn, context, fallback?) |
Calls fn(); if it throws, writes [motifjs Error - context] in development mode, passes the error to onUnexpectedError and returns fallback (or undefined). |
safeCallAsync(fn, context, fallback?) |
The same; fn returns a Promise, and the rejection is caught. |
safeCallSilent(fn, context) |
If it throws, writes to the console only in development mode; it does not go to onUnexpectedError or the listeners. |
errorHandler.safeCallSilentAsync, errorHandler.wrapSafe(fn, context), errorHandler.wrapSafeAsync(fn, context) |
An async silent call; return a new function that wraps fn with the same behaviour (reports like safeCall). |
context is a free label that only appears in the log.
Where is an error in user code caught?
| Place | Result |
|---|---|
A lifecycle hook (onConfig, onBuilt, onMounted …), initializeComponent, a ref callback, an Options API ctor, a motif.on('x:…') listener |
An MJX122 report; the component keeps being built. The rejected promise of an async hook is the same. |
A DOM/component event handler (onclick, motif.on, on:name) |
An MJX123 report; the other handlers of the same event run. |
An application event listener (app.on, onRouterChanged) |
An MJX123 report; the other listeners run and the navigation completes. |
effect, bindings.watch, a binding getter, a createComputed getter |
An MJX208 report; the other effects in the same flush run, and the effect that failed keeps tracking. That run has no value: onValue in effect(getter, onValue) is not called, and bindings (text/attr/class, a when/ternary/switchCase branch) keep their last healthy state. |
A route hook (onEntering, onEnter, onLeave, onUpdate, onShow) |
An MJX306 report; the navigation continues. |
A useGuard guard |
An MJX306 report, and the navigation is cancelled ({ ok: false, cancelled: true, reason: 'guard' }). |
A route component's constructor / an import() failure |
An MJX304 report and the fallbacks.error page (params.error). |
Virtualization dataRequest |
First load/refresh: errorTemplate or MJX207; a later page: MJX207 + getState().error. |
Lazy caller |
onError, then Fallbackview; if there is no Fallbackview, MJX126 is reported (cause = the original error). Cancellation (signal) is not reported. |
Several errors inside DisposableStore.clear/dispose |
MJX503 is thrown; the cause.errors array. |
An Emitter listener |
onUnexpectedError (default: rethrow) + the listeners. |
Development and production
Development (app.useDevelopment(true)) |
Production | |
|---|---|---|
Reports (MJX122 …) |
console + listeners | console + listeners |
Warnings (MJX2xx, the MJX3xx checks …) |
console.warn |
none |
safeCallSilent / reportSuppressed |
console | none |
Compiler contract mismatch (MJX121) |
console.warn |
none |
app.useLogging(false) turns off only the console output; the listeners keep working.
Next step
To wrap network and external resource calls with retries, timeouts and a circuit breaker, see Resilience.