Skip to main content

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

StatusMeaning
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

TriggerMeaning
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:

  1. Which fact changed to trigger the refetch
  2. Which constraint evaluated to true
  3. What dependencies the constraint tracks
  4. 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.
Previous
GraphQL

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management