santifer/career-ops · error · Error

hackernews: unexpected item response for thread

Error message

hackernews: unexpected item response for thread ${threadId}

What it means

After resolving the thread id, the provider fetches the thread item from the HN (Algolia items) API and requires a JSON object back. If the response is missing or not an object (null, array, string, error payload), fetch() throws this error naming the thread id. Non-object items mean the thread could not be retrieved and children cannot be parsed.

Solutions

  1. Open https://news.ycombinator.com/item?id={threadId} to confirm the thread still exists.
  2. Call the item endpoint directly (hn.algolia.com/api/v1/items/{id}) and inspect the raw response.
  3. Retry later if Algolia is rate-limiting or degraded; add a small backoff between requests.
  4. Update the item parsing path in providers/hackernews.mjs if the API shape changed.

Example fix

// before
const item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
// after (retry once on bad shape)
let item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
if (!item || typeof item !== 'object') item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
Defensive patterns

Strategy: type-guard

Validate before calling

function isItemObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
const item = await (await fetch(itemUrl(id))).json();
if (!isItemObject(item)) throw new Error(`bad item payload for ${id}`);

Type guard

const isHnItem = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && ('id' in v || 'children' in v);

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (/hackernews: unexpected item response/.test(err.message)) {
    console.error(`Thread item ${err.message.split('thread ')[1]} unusable — verify it still exists on news.ycombinator.com`);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: ctx.fetchJson(itemUrl(threadId)) returns null or a non-object: deleted/dead thread id, Algolia item API returning an error JSON or empty body, or a proxy response swallowed as a string.

Common situations: Stale cached thread id used after a thread was removed, Algolia item endpoint outage or rate limit, and redirects/blocked requests whose body parses to something other than the expected item object.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/6210d5140243ca7a. Report an issue: GitHub.

Appendix: source

Thrown at providers/hackernews.mjs:154

/** @type {Provider} */
export default {
  id: 'hackernews',

  async fetch(entry, ctx) {
    // Step 1: Find the latest "Who is hiring?" story id.
    const searchData = await ctx.fetchJson(SEARCH_URL, { redirect: 'error' });
    const threadId = resolveLatestThreadId(searchData);
    if (!threadId) {
      throw new Error('hackernews: could not find "Ask HN: Who is hiring?" thread in search results');
    }

    const threadHnUrl = `https://news.ycombinator.com/item?id=${threadId}`;

    // Step 2: Fetch the thread item (children = top-level job comments).
    const item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
    if (!item || typeof item !== 'object') {
      throw new Error(`hackernews: unexpected item response for thread ${threadId}`);
    }

    const children = /** @type {any} */ (item).children;
    if (!Array.isArray(children)) return [];

    // Step 3: Parse each comment.
    const jobs = [];
    for (const child of children) {
      // Skip deleted / dead / empty comments.
      if (!child || child.deleted || child.dead) continue;
      const text = typeof child.text === 'string' ? child.text : '';
      if (!text.trim()) continue;

      const parsed = parseHnComment(text, threadHnUrl);
      if (!parsed) continue;

      jobs.push({
        title: parsed.title,

View on GitHub (pinned to aac998c7ed)