santifer/career-ops · error · Error

${provider.id}: fetch() did not return an array

Error message

${provider.id}: fetch() did not return an array

What it means

After a provider's fetch() resolves, scan.mjs verifies Array.isArray(jobs) and throws a plain Error if not. Each ATS provider (Greenhouse, Lever, Ashby, local parser, etc.) is contractually required to return an array of job objects; a non-array return is a provider-contract violation indicating a bug in the provider adapter or an unexpected API response shape.

Source

Thrown at scan.mjs:2420

    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 9b17a8ac97)

Solutions

  1. Inspect the provider adapter for the named provider.id — ensure it returns an array (even if empty).
  2. If the upstream API envelope changed, normalize in the adapter: return payload.results || payload.jobs || [].
  3. Guard before the throw: coerce jobs = Array.isArray(jobs) ? jobs : (jobs ? [jobs] : []).
  4. Check the provider's error path — a swallowed exception returning undefined is a likely culprit.

Example fix

// before (provider adapter)
async fetch(company, ctx) {
  return await response.json(); // returns { jobs: [...] }
}

// after
async fetch(company, ctx) {
  const payload = await response.json();
  return Array.isArray(payload) ? payload : (payload.jobs || []);
}
Defensive patterns

Strategy: type-guard

Validate before calling

const jobs = await provider.fetch(company, ctx);
if (!Array.isArray(jobs)) {
  throw new TypeError(`${provider.id}: expected array, got ${typeof jobs}`);
}

Type guard

function isJobsArray(result) {
  return Array.isArray(result) && result.every(j => j && typeof j === 'object');
}

Try / catch

let jobs = await provider.fetch(company, ctx);
if (!Array.isArray(jobs)) {
  console.warn(`${provider.id} returned non-array (${typeof jobs}); coercing`);
  jobs = Array.isArray(jobs?.jobs) ? jobs.jobs : (jobs ? [jobs] : []);
}

Prevention

When it happens

Trigger: A provider returns null, undefined, an object, or a string instead of an array; an upstream ATS API changes its response envelope (e.g. wrapping jobs in {results: [...]}) and the adapter forwards it untransformed; a provider throws and a fallback returns a non-array.

Common situations: ATS vendor changes their API response format; a custom/local provider adapter that returns a single object for a one-job posting; a fetch that returns the raw Response object instead of parsed JSON.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/a5e444d3235b3715. Report an issue: GitHub.