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

  1. Inspect the provider's fetch() in providers/ and ensure every code path returns an array (return [] instead of null/undefined)
  2. Check the upstream ATS API response shape for recent changes and update the provider's normalization
  3. Log the actual returned value to identify which provider code path skipped array construction
  4. 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

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


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)