Mithril 2.x Data Layer Decision: m.request vs. Custom Fetch
A decision guide for Mithril 2.x apps: compare built-in m.request against a custom fetch wrapper for predictable redraws, testable cancellation, and consistent error shapes.
19 Sept 2026, 23:37 UTC

The Fork in the Road
If your Mithril 2.x single-page application talks to a REST or JSON API, you eventually have to choose between the framework's built-in m.request utility and a hand-rolled fetch wrapper. The decision is not about which tool is newer; it is about which trade-offs match your constraints. If bundle size and automatic redraws matter most, start with m.request. If you need interceptors, precise cancellation, or a shared error contract across transports, a thin wrapper or native fetch may be worth the extra plumbing. Because Mithril 2.x changed promise semantics relative to 1.x, verify the exact return shape and cancellation behavior against your installed version before you rely on either path.
Decision Constraints
Before comparing code, define what your data layer must guarantee. In a Mithril 2.x SPA, three constraints usually drive the choice:
- Predictable redraws: the view should update after asynchronous data arrives without extra ceremony.
- Testable cancellation: in-flight requests should not leak when a route or component unmounts.
- Consistent error shape: every caller should receive the same error object regardless of the underlying transport.
Supported Options at a Glance
| Option | Bundle impact | Redraw integration | Cancellation | Error shape | Best when |
|---|---|---|---|---|---|
m.request | Minimal (built-in) | Designed to integrate with Mithril's redraw cycle | Version-dependent; verify | Promise rejection | Small apps that want default behavior |
fetch + component state | Zero extra dependencies | Manual m.redraw or observable state container | AbortController (native) | You define it | You need standard web APIs and custom interceptors |
Thin m.request wrapper | Small wrapper overhead | Preserves m.request redraw behavior | Depends on wrapper | Normalized inside wrapper | You want retry or interceptors without leaving Mithril |
Trade-offs Explained
Built-in m.request. It minimizes dependencies and keeps the redraw cycle predictable because the utility is part of the framework lifecycle. The risk is API drift: Mithril 2.x altered promise semantics compared with 1.x, so the exact return shape, supported options, and cancellation hooks can differ from older examples you may find online.
Native fetch. It is universally understood, easy to mock in tests, and ships with the browser. The cost is that it can bypass Mithril's batching. You must explicitly call m.redraw after resolution, or wire the result into a state container that Mithril can observe. You also own cancellation via AbortController, which is standard but manual.
Thin wrapper. Wrapping m.request lets you add retry logic or interceptors while preserving the core API. The downside is that you now maintain a custom module, and you must be careful not to mix it with raw fetch unless you standardize error objects and cancellation across both paths.
Concrete Implementation with m.request
The following component illustrates the pattern: call m.request in oninit, store results in component state, and render loading, error, and success branches. Treat the option names below as a starting point and confirm them against your installed Mithril 2.x version.
var m = require("mithril")
var UserList = {
oninit: function() {
var state = { loading: true, error: null, users: null }
this.state = state
m.request({
method: "GET",
url: "/api/users"
// withCredentials: true // add only if your API requires it
}).then(function(result) {
state.loading = false
state.users = result
}).catch(function(error) {
state.loading = false
state.error = error
})
},
view: function() {
var state = this.state
if (state.loading) return m("p", "Loading…")
if (state.error) return m("p", "Error: " + state.error.message)
return m("ul", state.users.map(function(user) {
return m("li", { key: user.id }, user.name)
}))
}
}
Adding Retry Without Leaving Mithril
A retry wrapper can re-invoke the request promise and expose the last error for display. This keeps the transport decision unchanged while adding resilience.
function createRetryable(requestFn, maxRetries) {
var lastError = null
var attempt = function(remaining) {
return requestFn().catch(function(error) {
lastError = error
if (remaining <= 0) throw error
return attempt(remaining - 1)
})
}
return {
run: function() { return attempt(maxRetries) },
getLastError: function() { return lastError }
}
}
// Usage inside oninit:
// var retryable = createRetryable(function() {
// return m.request({ method: "GET", url: "/api/users" })
// }, 2)
// retryable.run().then(...).catch(...)
Validation Strategy
Before committing to either path, validate behavior in a scratch project with the exact version you ship.
- Install the pinned version. In your project root, run
npm install mithril@2 --save-exact. Then inspect the exported signature in your entry point or browser console:console.log(m.request.toString()). Confirm it accepts an options object and returns a promise. If the shape differs from the example above, adjust the component. - Unit-test state transitions. Use Jest or Vitest with a mocked
XMLHttpRequest. Mount the component and assert thatoninitruns once, that the loading branch renders first, and that either the success or error branch renders after the promise settles. If your installed version supports cancellation, unmount the component and assert the in-flight request is aborted. - Measure bundle impact. Run your production build with and without the chosen data layer.
m.requestis built-in, so the delta should be near zero. A custom wrapper adds only your code, but a third-party HTTP client would add measurable weight. - Check the official documentation. Confirm supported lifecycle hooks and request options for your installed major version. Mithril 2.x is not a drop-in replacement for 1.x in every detail.
Limitations and Practical Checks
Do not assume m.request has the same return shape in Mithril 1.x and 2.x. If you choose a custom fetch wrapper, ensure it triggers redraws or integrates with a state container that Mithril can observe; otherwise the view will not update. Avoid mixing m.request and fetch in one feature unless you standardize error objects and cancellation.
After wiring the component, open your browser's network tab and confirm exactly one request fires per mount. Trigger an error response and verify the error branch renders. If you implemented retry, confirm the retry count and that the UI remains responsive during retries. These checks catch the most common integration mistakes without requiring a full end-to-end suite.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.