Events
- Category: Core
Point0 emits a lifecycle event for every query, mutation, and fetch it runs.
Subscribe to them on any point with .on / .serverOn / .clientOn to log,
report errors, or collect metrics — without touching the loaders themselves. The
common case is one error subscriber on the root:
export const root = Point0.lets
.root()
.on('error', ({ side, name, error, meta }) => {
// 'error' is sugar for the four error events; meta is a log-friendly projection
console.error({ side, name, error, ...meta })
})
.root()Every failed query or mutation now reaches your handler, with side, the event
name, the typed error, and a slim meta you can spread straight in. The
rest of this page shows the full event set, the subscription methods, and the
event shape.
Subscribing
Three methods, all on every point type, all chainable (they return the same point, so add as many as you like):
.on('pointQuerySuccess', (e) => { /* kept in both bundles */ })
.serverOn('engineFetchError', (e) => { /* cut from the client bundle */ })
.clientOn('pointMutationSuccess', (e) => { /* cut from the server bundle */ }).on— not cut from either bundle: kept in both (isomorphic), so it fires on both sides..serverOn— cut from the client bundle: its body and the imports it uses are removed, so the handler never ships to the browser. (It fires only for server-side events.).clientOn— cut from the server bundle: body and its imports removed. (It fires only for client-side events.)
The side is decided when the event is emitted, from where the emitting code
runs — not from the event name. A .serverOn callback never sees a client-side
event, and vice versa.
The compiler cuts each side-bound callback from the bundle it doesn't belong in,
along with the imports that only that callback pulls in: a .serverOn callback
— body and its imports — is removed from the client bundle, so server secrets in
a .serverOn handler never reach the browser; a .clientOn callback — body and
imports — is removed from the server bundle. A plain .on callback is not cut
from either bundle: it's kept in both (isomorphic) and runs on both sides.
Subscriptions accumulate: each call adds to the list, and a point inherits every subscription from its parents up the chain. Put app-wide logging on the root and it covers everything beneath.
By name, by list, or everything
.on('pointQueryStart', (e) => { /* one event */ })
.on(['pointQuerySuccess', 'pointMutationSuccess'], (e) => { /* several */ })
.on('*', (e) => { /* every event */ })With '*' the callback receives the full event union; with a single name it's
narrowed to that event. The wildcard still respects the side filter —
.serverOn('*') fires only for server-side events.
The full event set
Six families, each with four lifecycle phases — Start, Settled, Success,
Error — plus one emitError event. Twenty-five events in all (six families ×
four phases, plus emitError); the table below lists the families, the
Reference enumerates every expanded name:
| Family | What it tracks | Side |
|---|---|---|
pointQuery* | a query running (useQuery / fetchQuery) | client | server |
pointInfiniteQuery* | an infinite query running | client | server |
pointMutation* | a mutation running | client | server |
pointFetchServer* | a point's server-fetch step (the SSR / fetch machinery) | client | server |
pointPrefetchPage* | a page being prefetched before navigation | client | server |
engineFetch* | the engine's outgoing HTTP fetch (the actual request) | server only |
emitError | a subscriber callback itself threw (see below) | client | server |
Each family gives you <Family>Start, <Family>Settled, <Family>Success, and
<Family>Error — e.g. pointQueryStart, pointQuerySettled,
pointQuerySuccess, pointQueryError.
Lifecycle phases
For any one run:
Startfires before the work begins.Settledfires on every outcome — success or error.Successfires on a successful result.Errorfires only on a genuine error.
// a query that succeeds: pointQueryStart → pointQuerySettled → pointQuerySuccess
// a query that throws: pointQueryStart → pointQuerySettled → pointQueryErrorOne edge case worth knowing: a redirect is a success, not an error. When a
loader redirects (throw redirect(...)), the query settles down the success
path — Settled then Success fire, not Error. See Navigation
for redirects.
Why engineFetch* is server-only
engineFetch* wraps the actual outgoing HTTP request, which only the server
makes — so those events are typed 'server' and are only reachable through
.on and .serverOn. Naming engineFetch* inside .clientOn is a type
error: it isn't in the client event set. This is the rule behind "server-only
events visible only in serverOn".
The other point* events report side: 'client' even during SSR, because the
query/fetch code that emits them is client-authored (it runs on the server under
SSR, but it's the same code). Only engineFetch* — the HTTP layer — reports
side: 'server'. During one SSR page load you'll see both:
.on('*', (e) => order.push([e.name, e.side]))
// pointQueryStart client
// pointFetchServerStart client
// engineFetchStart server ← the actual HTTP request
// engineFetchSettled server
// engineFetchSuccess server
// pointFetchServerSettled client
// pointFetchServerSuccess client
// pointQuerySettled client
// pointQuerySuccess clientThe 'error' shorthand
.on('error', cb) is sugar — it expands to four subscriptions, one per
error event:
.on('error', (e) => { /* … */ })
// equivalent to:
.on(['pointMutationError', 'pointQueryError', 'pointInfiniteQueryError', 'engineFetchError'], (e) => { /* … */ })Inside the callback, error is narrowed to a non-undefined error instance —
the typed reason these are the error events. One user throw can produce more
than one of them: a failing server query surfaces as engineFetchError (server)
and pointQueryError (client), so an .on('error') logger may see the same
failure from two angles.
GOTCHA: the shorthand covers query, infinite-query, mutation, and engineFetch errors — not
pointFetchServerErrororpointPrefetchPageError. To catch those two, name them explicitly:.on(['pointFetchServerError', 'pointPrefetchPageError'], cb).
The event object
Every callback receives one object with the same five fields:
.on('pointQueryError', ({ side, name, data, error, meta }) => {
side // => 'client' | 'server' — where the emitting code ran
name // => 'pointQueryError'
data // the raw payload — rich, but heavy to log
error // the typed error instance (your error class — ErrorPoint0 by default; undefined on non-error events)
meta // a slim, log-friendly projection of data
})side—'client'or'server', set at emit time (see side above).name— the event name.data— the full payload (the query result, the request object, theQueryClient, …). Accurate, but not pleasant to serialize. Prefermetafor logging.error— the error instance, hoisted to the top level so an'error'handler can read it directly. It's the same object asdata.error. It's the typederror(your error class) on error events, andundefinedon every other event (the key is always present).meta— the log-friendly projection, below.
meta: the log-friendly projection
meta is a plain record (Record<string, unknown>) built per event from
data, meant to go straight into a logger:
.on('pointQueryStart', ({ meta }) => {
meta.point // => 'root:page:home' — the point's id (<scope>:<type>:<name>), not the object
meta.input // => { id: 123 } — the input, sanitized (see below)
})It exists because data carries heavy objects (responses, requests, query
results) that you don't want in a log line. meta replaces them with compact
forms: points become their string id (<scope>:<type>:<name>), requests become
{ method, path }, errors and redirects are serialized, and it drops bulky
members. For an engineFetch event, meta.result and meta.response are
dropped; for the SSR case, a settled event's meta.request.renders reports
how many SSR render passes ran.
Note meta does not carry the error — on an error event, meta.error is
undefined. The error lives on the envelope error (and data.error). So a
typical error log spreads meta and adds the parts it needs:
.on('error', ({ side, name, error, meta }) => {
console.error({ ...meta, side, name, error })
})Binaries in the input are sanitized
meta.input runs through a sanitizer that replaces binary values with
placeholder strings, so file uploads never bloat or break a log line:
// a mutation input of { photo: File, note: 'hi' } logs as:
meta.input // => { photo: '[File: photo.png (5120 bytes)]', note: 'hi' }File → [File: <name> (<size> bytes)], Blob → [Blob: <size> bytes],
FormData → [FormData]. Nested binaries (inside arrays/objects) are replaced
too; everything else passes through unchanged.
Subscriber errors and emitError
Callbacks are fire-and-forget: they may be sync or async, and Point0 does not await them. A slow handler never blocks a request, and the completion order of async handlers across subscriptions isn't guaranteed.
If a callback throws, the framework does not crash. The error is caught and
re-emitted as an emitError event, carrying the original event and the thrown
error — so you can observe your own handler failures:
.on('emitError', ({ error, data, meta }) => {
// error — what your handler threw (coerced to the error class)
// data.event — the full original event that was being handled
// meta.event — its slim { name, meta } projection
console.error('event handler failed', data.event.name, error)
})A throw inside an emitError handler is swallowed silently — there's no
recursion, so a broken error reporter can't cause an emit loop.
Wiring events to your observability
Funnel events through one subscriber on the root and let your logging stack fan out. The simplest version just writes to the console:
export const root = Point0.lets
.root()
.on('error', ({ side, name, error, meta }) => {
console.error({ side, name, error, ...meta })
})
.root()Swap console.error for your own logger and you get error reporting with no
extra call sites at the points. In start0, for example, this subscriber writes
to a LogTape logger whose sink forwards error records to Sentry — the same
single root subscription, just a richer sink behind it.
Subscribe to the success/settled events the same way for request metrics or audit logs.
Reference
Event names
pointQueryStart pointQuerySettled pointQuerySuccess pointQueryError
pointInfiniteQueryStart pointInfiniteQuerySettled pointInfiniteQuerySuccess pointInfiniteQueryError
pointMutationStart pointMutationSettled pointMutationSuccess pointMutationError
pointFetchServerStart pointFetchServerSettled pointFetchServerSuccess pointFetchServerError
pointPrefetchPageStart pointPrefetchPageSettled pointPrefetchPageSuccess pointPrefetchPageError
engineFetchStart engineFetchSettled engineFetchSuccess engineFetchError
emitErrorSubscription methods
| Method | Fires on | Name argument accepts |
|---|---|---|
.on | both sides | any event name, 'error', '*', or an array of names |
.serverOn | server-side events | server event names, 'error', '*', array |
.clientOn | client-side events | client event names, 'error', '*', array |
All three are on every point type, accumulate, and are inherited down the chain.
engineFetch* names are valid in .on / .serverOn only — a type error in
.clientOn.
Event object fields
| Field | Type | Notes |
|---|---|---|
side | 'client' | 'server' | where the emitting code ran, set at emit time |
name | the event name | |
data | the raw payload (per event) | rich; not log-friendly |
error | error instance, or undefined | present on error events; same object as data.error |
meta | Record<string, unknown> | log-friendly projection of data |
Per-event data
| Family | data carries |
|---|---|
pointQuery* | { queryKey, point, input, mode, data?, error?, redirect? } |
pointInfiniteQuery* | same as query, for the infinite case |
pointMutation* | { point, input } + one of output / error / redirect |
pointFetchServer* | { input, point } + the fetch-server output on settled/success/error |
pointPrefetchPage* | { point, input, options, error? } |
engineFetch* | { request, scope, result?, error? } |
emitError | { error, event } — the original event and the thrown error |
Public types
The event types are exported from @point0/core: AnyEventerEvent,
EventerEvent, EventerEventMeta, EventerSide, and the per-event types. The
barrel is type-only (export type * from './eventer.js'), so the runtime
uniqEventerErrorEventNames constant — the four names the 'error' shorthand
expands to — is not importable as a value.
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️