Hmbown/CodeWhale · critical · Error

Whale point asset is unavailable.

Error message

Whale point asset is unavailable.

What it means

Thrown at startup when `fetch('./whale-points.tsv')` returns a non-ok HTTP status. The pet UI requires this asset to build the whale point cloud and cannot start the habitat without it, so the fetch failure is converted into this explicit error inside the startup try block.

Solutions

  1. Ensure `whale-points.tsv` exists next to the page and is served with a 200 status.
  2. Check the network tab for the actual status/URL of `./whale-points.tsv` and fix the deployment path.
  3. If using a service worker, add the asset to the precache list.

Example fix

// before: asset missing at ./whale-points.tsv
// after: copy it into the deploy output
cp assets/whale-points.tsv dist/whale-points.tsv
Defensive patterns

Strategy: try-catch

Validate before calling

const head = await fetch('./whale-points.tsv', { method: 'HEAD' });
if (!head.ok) showFatal('whale-points.tsv is not deployed.');

Type guard

null

Try / catch

try { await loadWhalePoints(); } catch (e) { showFatal('Pet assets failed to load: ' + e.message); }

Prevention

When it happens

Trigger: Opening pet.ts when `./whale-points.tsv` is missing from the served directory, the server returns 404/500, or a service worker/offline cache serves an error response.

Common situations: Deploying the UI without copying static assets, wrong base path / reverse proxy rewrite, offline PWA with uncached asset, dev server serving only bundled JS.

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 Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/fb1181e8a64fa647. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/ui/pet.ts:278

    liveTape.reset();
    if (!document.hidden && world && !restoring) { world.resumeObservation(); seconds = world.frame.timeMs / 1000; }
  }
  void persist(); if (audio) anchor = audio.currentTime - seconds + .08;
});
window.addEventListener('pagehide', () => { void persist(); });
function animate(now: number) {
  if (world && !restoring && !paused && !document.hidden) {
    accumulator += last ? Math.min(.1, (now - last) / 1000) : 0;
    while (accumulator >= 1 / 30 && (mode.value === 'live' || !tape.length || seconds < duration())) {
      world.step(1 / 30, { motion: !motion.checked, sensitivity: 1 }); seconds = world.frame.timeMs / 1000; play(world.voices); accumulator -= 1 / 30;
    }
    if ((mode.value === 'live' || !tape.length) && seconds >= duration()) seek.max = String(Math.ceil(seconds / 30) * 30 + 30);
    draw();
  }
  last = now; requestAnimationFrame(animate);
}
try {
  const response = await fetch('./whale-points.tsv'); if (!response.ok) throw new Error('Whale point asset is unavailable.');
  points = (await response.text()).trim().split('\n').map(row => row.trim().split(/\s+/).map(Number) as [number, number]);
  if (points.length !== 980 || points.some(p => p.length !== 2 || !p.every(Number.isFinite))) throw new Error('Invalid whale point asset.');
  try {
    const saved = await library.getHabitat();
    if (saved) {
      savedRevision = saved.revision; const h = saved.habitat;
      if (h.petPersistenceVersion !== 1 || !Number.isFinite(h.seconds) || h.seconds < 0 || h.seconds > PET_MAX_SECONDS
        || !['wild', 'demo', 'replay'].includes(h.source) || typeof h.still !== 'boolean' || typeof h.sourceName !== 'string') throw new Error('Saved habitat is invalid. Save a replay file before replacing it.');
      expressionVersion = h.expressionVersion === undefined ? 1 : h.expressionVersion;
      if (![1, 2].includes(expressionVersion) || h.checkpoint && (h.checkpoint.sim?.expressionVersion ?? 1) !== expressionVersion) throw new Error('Saved expression version does not match its checkpoint.');
      const savedWorld = PetWorld.fromRecording(points, { ...h, petReplayVersion: h.petReplayVersion === undefined ? 1 : h.petReplayVersion });
      if (h.checkpoint && savedWorld.frame.timeMs / 1000 !== h.seconds) throw new Error('Saved pet clock does not match its checkpoint.');
      tape = h.tape; interactions = [...h.interactions]; mode.value = h.source; motion.checked = h.still || media.matches;
      source.textContent = h.sourceName;
      seek.max = String(Math.max(h.source === 'demo' ? 80 : h.source === 'replay' ? 90 : 120, savedWorld.endTimeMs / 1000));
      if (h.source === 'replay') {
        mode.querySelector<HTMLOptionElement>('[value="replay"]')!.disabled = false;
      }

View on GitHub (pinned to 433685b202)