{"slug": "empty-is-a-result-loading-is-a-state", "title": "Empty is a result. Loading is a state.", "summary": "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.", "body_md": "The result element exists. It even contains text. Unfortunately, the text says `Loading...` instead. The check is green before the useful result has arrived.\n\nAn empty result needs a different answer from a result that is still loading. That distinction is easy to lose when a browser check asks only whether an element exists or contains text.\n\nFollowing a [discussion about browser readiness](https://dev.to/jaredchuvn/comment/3fomd), 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.\n\nEach check starts from the same element with `data-state=\"loading\"` and a `Loading...` placeholder. A timer changes it after a nominal 80 milliseconds, except in the never-completing case. Each check has a 250 millisecond waiting budget.\n\nThese are the observations from the October 1 run in HeadlessChrome 155 on macOS:\n\n| Outcome | Element exists | Text is nonempty | Explicit terminal state | \n|---|---|---|---|\n| One row | Accepted loading | Accepted loading | Accepted `alpha` | \n| Completed empty | Accepted loading | Accepted loading | Accepted empty text | \n| Error | Accepted loading | Accepted loading | Reported error | \n| Never completes | Accepted loading | Accepted loading | Timed out | \n\nThe 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.\n\nSave 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.\n\nRun the cases sequentially. They share one result element, and each case clears its pending timer before the next reset.\n\n```\n<!doctype html><meta charset=\"utf-8\"><title>Synthetic readiness fixture</title>\n<h1>Synthetic readiness fixture</h1><pre id=\"output\">Not run</pre><div id=\"result\"></div>\n<script>\nasync function runCase(mode, strategy) {\n  const root = document.querySelector('#result');\n  root.dataset.state = 'loading'; root.textContent = 'Loading...';\n  let timer;\n  if (mode !== 'never') timer = setTimeout(() => {\n    root.dataset.state = mode === 'error' ? 'error' : 'complete';\n    root.textContent = mode === 'rows' ? 'alpha' : mode === 'empty' ? '' : 'API error';\n  }, 80);\n  const start = performance.now();\n  let status = 'timeout';\n  while (performance.now() - start < 250) {\n    const observed = document.querySelector('#result');\n    if ((strategy === 'presence' && observed) || (strategy === 'nonempty' && observed?.textContent.trim())) {status = 'accepted'; break;}\n    if (strategy === 'terminal' && root.dataset.state === 'complete') {status = 'accepted'; break;}\n    if (strategy === 'terminal' && root.dataset.state === 'error') {status = 'error'; break;}\n    await new Promise(r => setTimeout(r, 10));\n  }\n  clearTimeout(timer);\n  return {mode, strategy, status, state:root.dataset.state, text:root.textContent, elapsed_ms:Math.round(performance.now()-start)};\n}\nasync function runAll() {\n  const results=[];\n  for(const mode of ['rows','empty','error','never']) for(const strategy of ['presence','nonempty','terminal']) results.push(await runCase(mode,strategy));\n  document.querySelector('#output').textContent=JSON.stringify(results,null,2);\n  return {userAgent:navigator.userAgent,results};\n}\n</script>\n```\n\nThe presence check calls [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), which returns a matching element or `null`. A match establishes presence. The loading element deliberately satisfies it.\n\nThe state check reads a custom [`data-*` attribute through `dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset). The application supplies its meaning. Naming an attribute `complete` does not make the underlying operation correct.\n\nFor an interface you own, make loading, 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.\n\nFor 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.\n\nThis 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.\n\nA 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 loading text, so its nonempty check fails earlier by accepting that placeholder.\n\nThe useful question for a readiness assertion is precise: what evidence shows that this operation finished, even when the correct result is empty?\n\n*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.*", "url": "https://wpnews.pro/news/empty-is-a-result-loading-is-a-state", "canonical_source": "https://dev.to/jaredchuvn/empty-is-a-result-loading-is-a-state-2l4h", "published_at": "2026-10-01 03:08:51+00:00", "updated_at": "2026-10-01 03:16:33.324795+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["HeadlessChrome", "macOS", "MDN"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/empty-is-a-result-loading-is-a-state", "markdown": "https://wpnews.pro/news/empty-is-a-result-loading-is-a-state.md", "text": "https://wpnews.pro/news/empty-is-a-result-loading-is-a-state.txt", "jsonld": "https://wpnews.pro/news/empty-is-a-result-loading-is-a-state.jsonld"}}