Data Fetching
•3 min read
Explain & Debug
explainQuery tells you exactly why a query is in its current state. No other data fetching library can do this – it requires a constraint engine underneath.
Basic Usage
import { explainQuery } from "@directive-run/query";
console.log(explainQuery(system, "user"));
Output:
Query "user"
Status: refetching in background (stale-while-revalidate)
Cache key: {"userId":"42"}
Data age: 45s
Last fetch causal chain:
Fact changed: userId "41" -> "42"
Constraint: _q_user_fetch (priority 50)
Dependencies: userId, _q_user_state, _q_user_key
Resolved in: 145ms
With createQuerySystem
The explain method is built in – no separate import needed:
const app = createQuerySystem({
facts: { userId: "" },
queries: { user: { key: ..., fetcher: ... } },
});
app.facts.userId = "42";
await app.settle();
console.log(app.explain("user"));
What It Shows
Status
| Status | Meaning |
|---|---|
pending (waiting for trigger) | Key is null or query is disabled |
fetching (first load) | First fetch, no cached data yet |
refetching in background (stale-while-revalidate) | Has cached data, fetching fresh data |
success (fresh) | Data is cached and within refetchAfter window |
success (fresh, becoming stale) | Data is marked stale but not yet refetched |
error (N failures) | Fetch failed, showing failure count |
Cache Key
The serialized key object that identifies this query's cache entry. Changes when the key function returns a different result.
Data Age
How long ago the data was last successfully fetched, in seconds.
Trigger Reason
| Trigger | Meaning |
|---|---|
manual (refetch/invalidate) | Someone called .refetch() or .invalidate() |
initial fetch (no cached data) | First fetch for this key |
awaiting key (query disabled or key is null) | Key function returned null |
keepPreviousData
When keepPreviousData is active, the output includes:
Showing previous data (keepPreviousData active)
Causal Chain
When trace is enabled on the system, explainQuery shows the full causal chain:
Last fetch causal chain:
Fact changed: userId "41" -> "42"
Constraint: _q_user_fetch (priority 50)
Dependencies: userId, _q_user_state, _q_user_key
Resolved in: 145ms
This tells you:
- Which fact changed to trigger the refetch
- Which constraint evaluated to true
- What dependencies the constraint tracks
- How long the resolver took
Enable Trace
For the full causal chain, enable trace on the system:
// createQuerySystem
const app = createQuerySystem({
facts: { userId: "" },
queries: { user: { key: ..., fetcher: ... } },
trace: true,
});
// Advanced path
const system = createSystem({
module: mod,
trace: true,
});
Debugging Tips
"Why did my query fire 3 times?"
console.log(app.explain("user"));
// Check the trigger reason and cache key.
// Common cause: the key function creates a new object on every call,
// causing the serialized key to differ even though the values are the same.
"My mutation invalidated but the query didn't refetch"
// Check that the query's tags match the mutation's invalidateTags
console.log(app.explain("user"));
// Look for: Cache key: null (query disabled)
// This means the key returned null – the query won't refetch until key is non-null.
"My data is stale but not refetching"
// Check refetchAfter. With refetchAfter: 0 (default), data is always stale
// and refetches on every trigger (focus, reconnect, interval).
// With refetchAfter: 30000, data is fresh for 30s after fetch.
console.log(app.explain("user"));
// Look for: Data age: 45s – if this exceeds refetchAfter, next trigger will refetch.

