koala73/worldmonitor · error · NhcQueryError

NHC layer ${layerId}: ${cause.message}

Error message

NHC layer ${layerId}: ${cause.message}

What it means

nhcQuery fetches an NHC ArcGIS GeoJSON layer with a 15-second timeout. When the response status is not ok it cancels the body and wraps httpRetryError(res) — which derives a message and retryability from the HTTP status — into NhcQueryError with message `NHC layer <layerId>: <cause.message>`. nonRetryable and retryAfterMs are inherited from the cause, so 429/5xx may be retried by withRetry while 4xx failures are terminal.

Solutions

  1. Read the wrapped cause.message (the httpRetryError result) to get the exact HTTP status, then act on it specifically.
  2. For 429, honor retryAfterMs (propagated onto the error) and add backoff/staggering between layer queries.
  3. For 404, resolve the current layer ID from the ArcGIS services directory and update NHC_STORM_SLOTS offsets.
  4. Retry later for 5xx/503 — the NHC service is often transiently unavailable; the withRetry wrapper may already cover retryable statuses.
  5. Verify network/proxy access to NHC_BASE from the machine running the seed.

Example fix

// before
const res = await fetchFn(url, { headers, signal });
// after: log status and back off on 429 before retrying the whole seed
const res = await fetchFn(url, { headers, signal });
if (res.status === 429) {
  const retryAfter = Number(res.headers.get('retry-after')) || 30;
  await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
Defensive patterns

Strategy: retry

Validate before calling

const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;
const head = await fetch(url, { method: 'HEAD' });
if (!head.ok) throw new Error(`layer ${layerId} unavailable: HTTP ${head.status}`); // fail before the full seed run
await head.body?.cancel?.();

Type guard

function isRetryableNhcFailure(err) {
  return err instanceof NhcQueryError && !err.nonRetryable;
}

Try / catch

try {
  const layer = await nhcQuery(layerId, expectedGeometryTypes);
  return layer;
} catch (err) {
  if (err instanceof NhcQueryError && err.retryAfterMs) {
    await sleep(err.retryAfterMs);
    return nhcQuery(layerId, expectedGeometryTypes); // single manual retry for 429
  }
  if (err instanceof NhcQueryError && err.nonRetryable) {
    logger.error({ layerId, cause: err.cause?.message }, 'NHC layer permanently unavailable');
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: The NHC layer query URL returns a non-2xx status: 404 after an NHC layer renumbering, 400 from a bad layerId, 429 rate limiting from repeated seed runs, 503 during NHC/ArcGIS maintenance, or any 5xx outage.

Common situations: Burst-running the seeder trips ArcGIS rate limits; NHC rotates service layer IDs between hurricane seasons so hardcoded slot indexes 404; corporate proxy blocks the request and returns an error page; NHC takes the GIS service offline during off-season maintenance.

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/fba2bbc8dd14d3bb. Report an issue: GitHub.

Appendix: source

Thrown at scripts/seed-natural-events.mjs:351

        code: 'NHC_POINT_RESPONSE_INVALID',
        nonRetryable: true,
      });
    }
  }
  return payload;
}

async function nhcQuery(layerId, expectedGeometryTypes, fetchFn = globalThis.fetch) {
  const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;
  return withRetry(async () => {
    const res = await fetchFn(url, {
      headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },
      signal: AbortSignal.timeout(15_000),
    });
    if (!res.ok) {
      await res.body?.cancel?.();
      const cause = httpRetryError(res, { remainingBudgetMs: 15_000 });
      throw new NhcQueryError(`NHC layer ${layerId}: ${cause.message}`, {
        cause,
        nonRetryable: cause.nonRetryable,
      });
    }
    let payload;
    try {
      payload = await res.json();
    } catch (cause) {
      const bodyTransportFailure = cause instanceof TypeError
        || cause?.name === 'AbortError'
        || cause?.name === 'TimeoutError';
      throw new NhcQueryError(
        `NHC layer ${layerId} ${bodyTransportFailure ? 'body read failed' : 'returned invalid JSON'}`,
        {
          code: bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID',
          cause,
          nonRetryable: !bodyTransportFailure,
        },

View on GitHub (pinned to 7d06c8633d)