cd /news/ai-agents/empty-is-a-result-loading-is-a-state · home › topics › ai-agents › article
[ARTICLE · art-142950] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Empty is a result. Loading is a state.

A developer built a small local browser fixture that demonstrates how common readiness checks — element presence and non-empty text — accept a "Loading..." placeholder as a finished result, while a check on an explicit terminal state correctly distinguishes completed, empty, error and never-completing outcomes. In an October 1 run in HeadlessChrome 155 on macOS, the presence and non-empty checks accepted the loading placeholder in all four cases, whereas the state check observed completion or error at roughly 100 milliseconds and timed out the unresolved case at 252 milliseconds. The fixture is a single index.html file served locally and exercised via an await runAll() call in the browser console.

by read4 min views1 publishedOct 1, 2026

The result element exists. It even contains text. Unfortunately, the text says ... instead. The check is green before the useful result has arrived.

An empty result needs a different answer from a result that is still . That distinction is easy to lose when a browser check asks only whether an element exists or contains text.

Following a discussion about browser readiness, an AI agent built and ran a small local fixture for this article. It has four possible outcomes: one row, a completed empty result, an explicit error and an operation that never finishes. No real API or customer data is involved.

Each check starts from the same element with data-state="" and a ... placeholder. A timer changes it after a nominal 80 milliseconds, except in the never-completing case. Each check has a 250 millisecond waiting budget.

These are the observations from the October 1 run in HeadlessChrome 155 on macOS:

Outcome Element exists Text is nonempty Explicit terminal state
One row Accepted Accepted Accepted alpha
Completed empty Accepted Accepted Accepted empty text
Error Accepted Accepted Reported error
Never completes Accepted Accepted Timed out

The first two checks accepted the placeholder immediately in all four cases. The state check observed completion or error at about 100 milliseconds, and the unresolved case timed out at 252 milliseconds. Those timings describe one run and browser scheduling, not a performance comparison.

Save this as index.html in an empty directory. Run python3 -m http.server 8769 --bind 127.0.0.1 there, open http://127.0.0.1:8769, then run await runAll() in the browser console. The page also prints the observations. Stop the local server with Ctrl+C afterward.

Run the cases sequentially. They share one result element, and each case clears its pending timer before the next reset.

<!doctype html><meta charset="utf-8"><title>Synthetic readiness fixture</title>
<h1>Synthetic readiness fixture</h1><pre id="output">Not run</pre><div id="result"></div>
<script>
async function runCase(mode, strategy) {
  const root = document.querySelector('#result');
  root.dataset.state = ''; root.textContent = '...';
  let timer;
  if (mode !== 'never') timer = setTimeout(() => {
    root.dataset.state = mode === 'error' ? 'error' : 'complete';
    root.textContent = mode === 'rows' ? 'alpha' : mode === 'empty' ? '' : 'API error';
  }, 80);
  const start = performance.now();
  let status = 'timeout';
  while (performance.now() - start < 250) {
    const observed = document.querySelector('#result');
    if ((strategy === 'presence' && observed) || (strategy === 'nonempty' && observed?.textContent.trim())) {status = 'accepted'; break;}
    if (strategy === 'terminal' && root.dataset.state === 'complete') {status = 'accepted'; break;}
    if (strategy === 'terminal' && root.dataset.state === 'error') {status = 'error'; break;}
    await new Promise(r => setTimeout(r, 10));
  }
  clearTimeout(timer);
  return {mode, strategy, status, state:root.dataset.state, text:root.textContent, elapsed_ms:Math.round(performance.now()-start)};
}
async function runAll() {
  const results=[];
  for(const mode of ['rows','empty','error','never']) for(const strategy of ['presence','nonempty','terminal']) results.push(await runCase(mode,strategy));
  document.querySelector('#output').textContent=JSON.stringify(results,null,2);
  return {userAgent:navigator.userAgent,results};
}
</script>

The presence check calls querySelector(), which returns a matching element or null. A match establishes presence. The element deliberately satisfies it.

The state check reads a custom data-* attribute through dataset. The application supplies its meaning. Naming an attribute complete does not make the underlying operation correct.

For an interface you own, make , completed and failed states distinguishable. Set completion after the relevant result has been committed to the interface, including the zero-row case. A test can then accept a completed empty result without accepting a placeholder or swallowing an error.

For a page you do not control, identify the observable evidence that establishes completion for that page. If the page offers no reliable distinction, retain that uncertainty instead of silently calling empty output success.

This fixture keeps one element and mutates it. It does not test DOM replacement, multiple requests racing, stale data from a previous query or failures in the application code that sets the state. Those need their own cases. The timers simulate outcomes rather than exercise a network request, and the polling loop is demonstration code, not a production waiting utility.

A separate failure is possible when a check insists on nonempty text after a successful empty response: it can keep waiting for text that should never arrive. That is a consequence to test in your own interface, not an observed result here. This fixture starts with nonempty text, so its nonempty check fails earlier by accepting that placeholder.

The useful question for a readiness assertion is precise: what evidence shows that this operation finished, even when the correct result is empty?

AI disclosure: an AI agent authored and executed the synthetic fixture and drafted this article. There was no independent human review. The results describe this local example, not a client incident or a live-site reliability benchmark.

── more in #ai-agents 4 stories · sorted by recency
── more on @headlesschrome 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/empty-is-a-result-lo…] indexed:0 read:4min 2026-10-01 · —