santifer/career-ops · error · Error

joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.

Error message

joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}

What it means

Thrown by joinup.fetch() when the __NEXT_DATA__ script tag was found (regex matched) but JSON.parse() of its contents threw, OR the parsed object's deep path (props.pageProps.serverState.initialResults.jobs.results) did not yield a usable hits array. The error message chains the underlying parse/access error so the root cause is visible.

Source

Thrown at providers/joinup.mjs:59

    try { host = new URL(entry.careers_url || '').hostname; } catch { return null; }
    return /(^|\.)joinup\.ch$/i.test(host) ? { url: BROWSE_URL } : null;
  },

  async fetch(entry, ctx) {
    // redirect:'error' — BROWSE_URL is pinned to joinup.ch (https); a 3xx must
    // not be followed to a private/metadata IP (matches every other provider).
    const html = await ctx.fetchText(BROWSE_URL, { redirect: 'error' });
    const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
    // Fail closed: a missing/unparseable __NEXT_DATA__ is a scraper break, not an
    // empty board — throw so the scan logs it instead of silently reporting zero.
    if (!m) throw new Error(`joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?)`);
    let hits = [];
    try {
      const data = JSON.parse(m[1]);
      const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;
      hits = Array.isArray(ir) && ir[0]?.hits ? ir[0].hits : [];
    } catch (err) {
      throw new Error(`joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}`);
    }
    return hits
      .filter(h => h && h.slug && (h.title || h.headline))
      .map(h => ({
        title: h.title || h.headline || '',
        url: `https://joinup.ch/job/${h.slug}`,
        company: h.startup || entry.name || '',
        location: typeof h.location === 'string' ? h.location
          : (h.location?.name || h.location?.city || ''),
        postedAt: toEpochMs(h.created),
      }));
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Capture the raw __NEXT_DATA__ content for the failing entry and validate it with a JSON linter.
  2. If the structure changed, update the data-access path (props.pageProps.serverState.initialResults.jobs.results) to the new nesting.
  3. If the JSON is genuinely truncated, investigate whether a content-encoding or transfer issue is cutting the response.
Defensive patterns

Strategy: try-catch

Validate before calling

function joinupPayloadOk(html) {
  const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
  if (!m) return false;
  try {
    const ir = JSON.parse(m[1])?.props?.pageProps?.serverState?.initialResults?.jobs?.results;
    return Array.isArray(ir) && (!ir[0] || Array.isArray(ir[0].hits));
  } catch { return false; }
}

Type guard

/** @param {any} data @returns {boolean} */
function joinupDataHasHits(data) {
  const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;
  return Array.isArray(ir) && (!ir[0] || Array.isArray(ir[0].hits));
}

Try / catch

try {
  jobs = await provider.fetch(entry, ctx);
} catch (err) {
  if (/failed to parse __NEXT_DATA__/.test(err.message)) {
    console.error(`joinup ${entry.name}: JSON parse failed — ${err.message}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: The __NEXT_DATA__ script contains malformed JSON (truncated, encoded, or injected with non-JSON content); the JSON parses but the nested serverState/initialResults structure differs from what the provider expects (keys renamed or moved); results is present but the inner hits array is absent so the fallback yields [].

Common situations: joinup.ch changed its server-state nesting (renamed initialResults or restructured results); a partial page render truncated the script content; a different page type was served that embeds a valid but differently-shaped __NEXT_DATA__.

Understand the failure class

Related errors


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