Hmbown/CodeWhale · error · Error

Invalid JSON on nonempty line

Error message

Invalid JSON on nonempty line ${i + 1}. Import cancelled; no rows were skipped.

What it means

When the whole input is not a single valid JSON document, importTrace falls back to line-delimited JSON (JSONL). Each nonempty line must parse; the first line that fails aborts the entire import with this error naming the 1-based line number, guaranteeing no rows are skipped silently.

Solutions

  1. Open the file at the reported line number and fix or remove the malformed line.
  2. Verify the file is valid JSON (single document) or strict JSONL (one JSON object per line); re-export if truncated.
  3. Validate each line with JSON.parse before calling importTrace to locate all bad lines at once.
  4. If the source is pretty-printed JSON, do not split it into lines yourself — pass it whole.

Example fix

// before
const text = lines.join(''); // concatenated objects, invalid JSONL
importTrace(text);
// after
const text = lines.filter(l => l.trim()).map(l => JSON.stringify(JSON.parse(l))).join('\n');
importTrace(text);
Defensive patterns

Strategy: validation

Validate before calling

function validateJsonOrJsonl(text: string): void {
  try { JSON.parse(text); return; } catch {}
  text.split(/\r?\n/).filter(l => l.trim()).forEach((l, i) => {
    try { JSON.parse(l); } catch { throw new Error(`Invalid JSON on line ${i + 1}`); }
  });
}

Type guard

function isJsonLine(line: string): boolean {
  try { JSON.parse(line); return true; } catch { return false; }
}

Try / catch

try {
  importTrace(text);
} catch (e) {
  const m = /Invalid JSON on nonempty line (\d+)/.exec(e.message);
  if (m) console.error(`Malformed JSONL at line ${m[1]}; fix or drop that line.`);
}

Prevention

When it happens

Trigger: importTrace with mixed/truncated JSONL (a partially written last line), plain text or CSV passed as JSON, pretty-printed JSON whose lines are individually invalid (it fails JSON.parse then each line fails), or concatenated JSON objects without newline separation.

Common situations: File truncated by a crash or incomplete download; wrong file selected (a log or CSV instead of JSONL); editing a JSONL file by hand; export written without a trailing flush.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at pet/src/core/ingest.ts:231

  }
  return { events, origins: new Map([...bases].map(([k, v]) => [k, v.toString()])), warnings };
}

/** Parse strictly: malformed lines or duplicate identities never disappear silently. */
export function importTrace(text: string, filename = 'Imported trace', options: ImportOptions = {}): Trace[] {
  const mode = options.privacy ?? 'redact', maxEvents = options.maxEvents ?? 250_000;
  if (!['redact', 'metadata', 'retain'].includes(mode)) throw new Error('Unknown privacy mode.');
  const maxTraces = options.maxTraces ?? 8;
  if (!Number.isInteger(maxEvents) || maxEvents < 1 || maxEvents > 250_000) throw new Error('maxEvents must be in [1, 250000].');
  if (!Number.isInteger(maxTraces) || maxTraces < 1 || maxTraces > 64) throw new Error('maxTraces must be in [1, 64].');
  if (new TextEncoder().encode(text).length > (options.maxBytes ?? 64 * 1024 * 1024)) throw new Error('File exceeds the 64 MiB MVP import limit. Split the export by trace.');
  const trimmed = text.replace(/^\uFEFF/, '').trim();
  if (!trimmed) throw new Error('The trace file is empty.');
  let document: unknown;
  try { document = JSON.parse(trimmed); }
  catch {
    document = trimmed.split(/\r?\n/).filter(l => l.trim()).map((line, i) => {
      try { return JSON.parse(line); } catch { throw new Error(`Invalid JSON on nonempty line ${i + 1}. Import cancelled; no rows were skipped.`); }
    });
  }
  // Transform before both normalization and raw retention, so raw cannot bypass redaction.
  const safe = mode === 'retain' ? (options.redactor ? options.redactor(document, '') : document) : redact(document, '', options.redactor);
  const root = obj(safe);
  if(root.format === 'whalesong.evidence/v1') return [evidenceToTrace(validateBundle(root, Math.min(maxEvents, 100_000)))];
  if (isCodewhaleSession(safe)) {
    const trace = fromCodewhaleSession(safe, filename, maxEvents);
    trace.privacy = mode;
    trace.events = trace.events.map(e => privacyEvent(e, mode));
    return [trace];
  }
  const records = Array.isArray(safe) ? safe : Array.isArray(root.events) ? root.events : null;
  if (isCodewhaleRuntimeDocument(records ?? [safe])) {
    const trace = fromCodewhaleRuntime(records ?? [safe], filename, maxEvents);
    trace.privacy = mode;
    trace.events = trace.events.map(e => privacyEvent(e, mode));
    return [trace];

View on GitHub (pinned to 433685b202)