santifer/career-ops · error · Error
: fetch() did not return an array
Error message
${provider.id}: fetch() did not return an array What it means
scan.mjs validates that each ATS provider's fetch() returns an array of job postings before accumulating results; this error is thrown when a provider returns null, undefined, or any non-array value. It is an internal contract check that turns a malformed provider response into a per-company error instead of corrupting the totals.
Solutions
- Inspect the provider's fetch() in providers/ and ensure every code path returns an array (return [] instead of null/undefined)
- Check the upstream ATS API response shape for recent changes and update the provider's normalization
- Log the actual returned value to identify which provider code path skipped array construction
- Wrap provider parsing so fallback paths always assign an array to `jobs` before the Array.isArray check
Example fix
// before (provider fetch)
async fetch() {
if (!boardId) return; // returns undefined
const jobs = await api.get(boardId);
return jobs.map(normalize);
}
// after
async fetch() {
if (!boardId) return [];
const jobs = await api.get(boardId);
return Array.isArray(jobs) ? jobs.map(normalize) : [];
} Defensive patterns
Strategy: type-guard
Validate before calling
const jobs = await provider.fetch();
if (!Array.isArray(jobs)) {
throw new Error(`${provider.id}: fetch() returned ${jobs === null ? 'null' : typeof jobs}, expected array`);
} Type guard
function isJobArray(v) {
return Array.isArray(v) && v.every(j => j && typeof j === 'object' && 'title' in j);
} Try / catch
try {
jobs = await provider.fetch();
} catch (err) {
errors.push({ company: company.name, error: `provider fetch failed: ${err.message}` });
jobs = []; // never leave jobs undefined before the Array.isArray check
}
if (!Array.isArray(jobs)) jobs = []; Prevention
- Every provider fetch() path must `return []` rather than null/undefined on empty or error paths
- Add a provider conformance test asserting fetch() resolves to an array
- Re-verify provider normalization whenever an upstream ATS API changes its response envelope
When it happens
Trigger: A provider module's fetch() returns undefined on an unhandled code path, returns an object instead of an array (API shape change), or a local parser failure paths leaves `jobs` unset before the API fallback assigns it.
Common situations: Upstream ATS API changes its response envelope so the provider's normalization returns a non-array; a provider bug where an early return skips array construction; a provider returning `null` to signal 'no board' instead of an empty array.
Related errors
- 4dayweek: unexpected API response on page
- a16z-speedrun-talent: unexpected API response on page
- agentic-jobs: unexpected API response shape on page
- API error
- arbeitnow: unexpected API response on page
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/a5e444d3235b3715.
Report an issue: GitHub.
Appendix: source
Thrown at scan.mjs:3136
let sourceName = provider.id === 'local-parser' ? 'local-parser' : `${provider.id}-api`;
try {
let jobs;
try {
jobs = await provider.fetch(company, ctx);
} catch (parserErr) {
if (provider.id !== 'local-parser') throw parserErr;
const fallback = resolveProvider(company, providers, { skipIds: ['local-parser'] });
if (!fallback || fallback.error) throw parserErr;
provider = fallback.provider;
sourceName = `${provider.id}-api`;
jobs = await provider.fetch(company, ctx);
errors.push({
company: company.name,
error: `local parser failed, used API fallback: ${parserErr.message}`,
});
}
if (!Array.isArray(jobs)) {
throw new Error(`${provider.id}: fetch() did not return an array`);
}
totalFound += jobs.length;
if (!company._isBoard && jobs.length === 0) {
emptyTargets.push(company.name);
}
for (const job of jobs) {
// Trust enrichment — runs before filters, never drops
const trustResult = trustValidator(job);
job.trustScore = trustResult.score;
job.trustFlags = trustResult.flags;
job.trustLevel = trustResult.level;
// Company blacklist (#1742) — the user's own do-not-apply decision,
// checked first: it's company-level, not a per-posting signal. Never
// silent: skips are counted and reported in the run summary, and
// --include-blacklisted lets the posting through annotated instead.
if (blacklist.size > 0) {View on GitHub (pinned to e7abd431fc)