santifer/career-ops · error · Error
Existing candidates file at ${candidatesPath} is not a JSON
Error message
Existing candidates file at ${candidatesPath} is not a JSON array What it means
Thrown by appendCandidate() when data/reply-candidates.json parses successfully but the top-level value is not a JSON array (e.g. an object, a string, a number). The contract is an array of candidate objects; a non-array base would break every downstream reader of reply-candidates.json.
Source
Thrown at paste-reply.mjs:123
};
}
/**
* Append a candidate to the candidates JSON file, creating the file/array if
* missing, without disturbing any existing entries. Exported for direct unit
* testing. Returns the total candidate count after the append.
*/
export function appendCandidate(candidate, candidatesPath = CANDIDATES_PATH) {
let candidates = [];
if (fs.existsSync(candidatesPath)) {
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(candidatesPath, 'utf-8'));
} catch (e) {
throw new Error(`Could not parse existing candidates file at ${candidatesPath}: ${e.message}`);
}
if (!Array.isArray(parsed)) {
throw new Error(`Existing candidates file at ${candidatesPath} is not a JSON array`);
}
candidates = parsed;
} else {
fs.mkdirSync(path.dirname(candidatesPath), { recursive: true });
}
candidates.push(candidate);
// Write-then-rename so an interrupted write (crash, signal, disk full)
// can never leave the real candidates file truncated/corrupted.
const tmpPath = `${candidatesPath}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(candidates, null, 2), 'utf-8');
fs.renameSync(tmpPath, candidatesPath);
return candidates.length;
}
// Collect subject/from/body from stdin via a single readline.Interface and a
// tiny manual state machine driven off its 'line' event.
//
// Earlier drafts chained `rl.question()` calls (one interface per prompt, orView on GitHub (pinned to 9b17a8ac97)
Solutions
- Inspect the file's top-level structure and convert it to a plain array of candidate objects.
- If unsure of the intended contents, back up the file and replace it with an empty array [].
- Confirm no other writer is serializing a non-array shape.
- Add a guard in your own writers to always pass arrays to JSON.stringify.
Example fix
// before: top-level object
{ "last": { "company": "Acme" } }
// after: top-level array
[ { "company": "Acme" } ] Defensive patterns
Strategy: type-guard
Validate before calling
import { readFileSync } from 'fs';
function candidatesIsArray(p) {
try { return Array.isArray(JSON.parse(readFileSync(p, 'utf8'))); }
catch { return false; }
} Type guard
/** Confirms the candidates file's top-level value is the expected array shape. */
function candidatesIsArray(p) {
try {
const v = JSON.parse(readFileSync(p, 'utf8'));
return Array.isArray(v);
} catch {
return false;
}
} Try / catch
try {
appendCandidate(c);
} catch (e) {
if (/is not a JSON array/.test(e.message)) {
// rewrite file as a valid array (with old contents wrapped if recoverable)
} else throw e;
} Prevention
- Ensure every writer of reply-candidates.json serializes a bare array.
- Add a schema test that asserts the file's top-level is an array.
- Reject wrapped-object shapes at the data boundary.
When it happens
Trigger: The file was overwritten with a single JSON object instead of an array; someone saved `{...}` or a bare string/number as the top-level value; a serialization bug elsewhere wrote the wrong shape.
Common situations: External tool wrote a wrapped object ({candidates:[...]}) instead of the bare array; a test fixture used the wrong shape; an older code path serialized differently.
Related errors
- Could not parse existing candidates file at ${candidatesPath
- payload must include at least one of: cv, articleDigest
- payload.cv requires { section, entry }
- payload.articleDigest requires { entry }
- hackernews: unexpected item response for thread ${threadId}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/5038e568f13ec38b.
Report an issue: GitHub.