koala73/worldmonitor · error · Error

Search unavailable

Error message

Search unavailable

What it means

loadIndex lazily fetches /sources/search-index.json to build the sources search index. If the response is not ok (404, 500, etc.) it throws 'Search unavailable'. The promise is reset to null in the catch so a later call retries the fetch. The thrown error propagates to callers awaiting the index (e.g. apply()).

Solutions

  1. Verify /sources/search-index.json is reachable in the browser (network tab) and check its HTTP status.
  2. Regenerate/deploy the search index as part of the build (ensure the crawlable-sources-page/index build step ran).
  3. Since loadIndex resets indexPromise on failure, trigger a retry by re-invoking the search action after fixing the file.
  4. If the path is wrong relative to the site base URL, fix the fetch path or server rewrite.
Defensive patterns

Strategy: retry

Validate before calling

const res = await fetch('/sources/search-index.json', { method: 'HEAD' });
if (!res.ok) {
  console.warn('search index missing; disabling search UI');
  disableSearch();
}

Try / catch

try {
  const index = await loadIndex();
} catch (e) {
  if (e.message === 'Search unavailable') {
    showSearchFallback('Search is temporarily unavailable.');
    return; // loadIndex resets its promise, so a later user action will retry
  }
  throw e;
}

Prevention

When it happens

Trigger: GET /sources/search-index.json returns a non-2xx status — the index file was not generated/deployed, the path 404s, or the server errors on that route.

Common situations: Deploy pipeline skipped the index build step, the static file is missing from the published assets, an SPA server rewrite returns a 404 for the JSON path, or the site is partially deployed.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/7745377e75fbeca7. Report an issue: GitHub.

Appendix: source

Thrown at scripts/crawlable-sources-search.mjs:16

// Runs only on the sources directory. Provider details stay on static pages.
function initSourcesSearch() {
  const search = document.getElementById('source-search');
  const fields = ['domain', 'kind', 'country', 'coverage'].map((name) => document.getElementById('source-' + name));
  const results = document.getElementById('source-results');
  const grid = document.getElementById('source-catalog');
  const more = document.getElementById('source-more');
  const note = document.getElementById('source-country-note');
  const noResults = document.getElementById('source-no-results');
  let indexPromise;
  let revision = 0;
  let offset = 0;
  const loadIndex = () => {
    if (!indexPromise) indexPromise = fetch('/sources/search-index.json')
      .then((response) => {
        if (!response.ok) throw new Error('Search unavailable');
        return response.json();
      }).catch((error) => { indexPromise = null; throw error; });
    return indexPromise;
  };
  const apply = async (nextPage = false) => {
    const request = ++revision;
    if (!nextPage) offset = 0;
    const query = search.value.trim().toLowerCase();
    const [domain, kind, country, coverage] = fields.map((field) => field.value);
    grid.replaceChildren();
    more.hidden = true;
    noResults.hidden = true;
    note.hidden = country === 'all' || country === 'intl';
    note.textContent = note.hidden ? '' : 'This list shows monitored sources based in the selected country or region. Sources based elsewhere also cover it.';
    if (!query && fields.every((field) => field.value === 'all')) {
      results.textContent = 'Choose a filter or enter a provider name.';
      return;
    }

View on GitHub (pinned to 7d06c8633d)