Hmbown/CodeWhale · error · Error

Pet recorder is closed or already writing.

Error message

Pet recorder is closed or already writing.

What it means

append() throws this when the recorder has been closed (output handle undefined) or when a previous append is still in flight (busy flag set). The recorder allows only one write at a time and no writes after close.

Solutions

  1. Await every append() call before issuing the next: `await recorder.append(bucket)`.
  2. Check lifecycle: do not call append() after close(); stop producers before closing.
  3. Serialize appends through a queue or use one writer task that owns the recorder.
  4. If closing early is the cause, defer close() until all pending writes resolve.

Example fix

// before
recorder.append(bucket); recorder.append(bucket2);
// after
await recorder.append(bucket);
await recorder.append(bucket2);
Defensive patterns

Strategy: type-guard

Validate before calling

function canAppend(recorder) { return recorder !== null && recorderClosed !== true; }

Type guard

function isWritable(rec) {
  return rec && typeof rec.append === 'function' && !closed && !pendingWrite;
}

Try / catch

try {
  await recorder.append(bucket);
} catch (e) {
  if (e.message.includes('closed or already writing')) {
    // serialize: wait for prior write or treat recorder as closed
    await pendingWrite?.catch(() => {});
  } else throw e;
}

Prevention

When it happens

Trigger: Calling append() after close() returned; calling append() again without awaiting the previous append() promise; calling append() while a rotation write is in progress.

Common situations: Fire-and-forget append calls in an event loop without awaiting; a shutdown path closing the recorder while producers still emit buckets; missing async/await on the prior append.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/c8722437fb16e5e3. Report an issue: GitHub.

Appendix: source

Thrown at pet/scripts/lib/pet-recorder.mjs:99

        throw new Error('The previous pet recording changed while reading; it was preserved.');
      bytes = length; restart = true; expectedMtime = original.mtimeNs;
      // Continue archive numbering without collecting a growing directory list.
      const prefix = `${basename(path)}.segment-`;
      for await (const entry of await opendir(dirname(path))) {
        if (!entry.name.startsWith(prefix)) continue;
        const suffix = entry.name.slice(prefix.length);
        if (!/^[0-9]{6,}\.jsonl$/.test(suffix)) continue;
        const number = Number(suffix.slice(0, -6));
        if (!Number.isSafeInteger(number) || number >= Number.MAX_SAFE_INTEGER)
          throw new Error('Pet archive numbering is exhausted; existing files were preserved.');
        segment = Math.max(segment, number);
      }
    }
    expectedMtime ??= (await output.stat({ bigint: true })).mtimeNs;
  } catch (error) { try { await output?.close(); } finally { lock.close(); } throw error; }
  return {
    async append(bucket) {
      if (!output || busy) throw new Error('Pet recorder is closed or already writing.');
      validatePetBucket(bucket);
      busy = true;
      try {
        await lock.check();
        const held = await output.stat({ bigint: true }), current = await lstat(path, { bigint: true });
        if (!current.isFile() || held.dev !== current.dev || held.ino !== current.ino || held.size !== BigInt(bytes) || held.mtimeNs !== expectedMtime)
          throw new Error('The live pet recording was changed or replaced externally; existing files were preserved.');
        const encode = seq => encodePetJSONL([{ ...bucket, sequence: seq, simTimeMs: seq * PET_BIN_MS }]);
        let row = encode(sequence), size = Buffer.byteLength(row);
        if (restart || sequence >= maxBuckets || bytes + size > maxBytes) {
          row = encode(0); size = Buffer.byteLength(row);
          if (size > maxBytes) throw new Error('Pet bucket exceeds the recording segment byte limit.');
          const temporary = resolve(dirname(path), `.${basename(path)}.next-${randomUUID()}`);
          const archive = `${path}.segment-${String(segment + 1).padStart(6, '0')}.jsonl`;
          let next, installed = false, created = false;
          try {
            next = await open(temporary, 'wx', 0o600); created = true;
            await next.writeFile(row); await next.sync();

View on GitHub (pinned to 433685b202)