{"record":{"id":"8b23e6015bf71b7b","repo":"heygen-com/hyperframes","slug":"chain-file-is-missing-a-nodes-array","errorCode":null,"errorMessage":"Chain file is missing a `nodes` array.","messagePattern":"Chain file is missing a `nodes` array\\.","errorType":"validation","errorClass":"AudioFxChainError","httpStatus":null,"severity":"error","filePath":"packages/core/src/audioFx.ts","lineNumber":775,"sourceCode":" * chain that silently loses a node would render differently from the project\n * the author saved, which is worse than refusing to render at all.\n */\nexport function parseAudioFxChain(json: string): HfAudioFxChain {\n  let raw: unknown;\n  try {\n    raw = JSON.parse(json);\n  } catch (err) {\n    throw new AudioFxChainError(`Chain file is not valid JSON: ${(err as Error).message}`);\n  }\n  if (typeof raw !== \"object\" || raw === null) {\n    throw new AudioFxChainError(\"Chain file must be a JSON object.\");\n  }\n  const obj = raw as { version?: unknown; nodes?: unknown };\n  if (obj.version !== HF_AUDIO_FX_CHAIN_VERSION) {\n    throw new AudioFxChainError(`Unsupported chain version: ${String(obj.version)}`);\n  }\n  if (!Array.isArray(obj.nodes)) {\n    throw new AudioFxChainError(\"Chain file is missing a `nodes` array.\");\n  }\n  const nodes: HfAudioFxNode[] = obj.nodes.map((n, i) => {\n    if (typeof n !== \"object\" || n === null) {\n      throw new AudioFxChainError(`Node ${i} is not an object.`);\n    }\n    const node = n as { type?: unknown; enabled?: unknown; params?: unknown };\n    if (typeof node.type !== \"string\" || !BY_ID.has(node.type)) {\n      throw new AudioFxChainError(`Node ${i} has unknown effect type: ${String(node.type)}`);\n    }\n    return {\n      type: node.type,\n      enabled: node.enabled !== false,\n      params: normalizeAudioFxParams(\n        node.type,\n        (node.params ?? undefined) as HfAudioFxParamValues | undefined,\n      ),\n    };\n  });","sourceCodeStart":757,"sourceCodeEnd":793,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/core/src/audioFx.ts#L757-L793","documentation":"After version checks pass, parseAudioFxChain() requires obj.nodes to be an Array. A missing nodes field, or nodes set to an object/scalar, is rejected. The nodes array is the core payload of a chain — without it there is nothing to render.","triggerScenarios":"A chain object that omits nodes entirely ({ version: 1 }); nodes spelled incorrectly (e.g. \"node\" or \"effects\"); nodes set to an object or single node instead of an array.","commonSituations":"Manual authoring with a typo'd key; a file truncated after the version field; confusion about the schema key name.","solutions":["Ensure the object has a top-level \"nodes\" key whose value is a JSON array (empty array [] is valid and yields a pass-through chain).","Use serializeAudioFxChain to emit the correct key."],"exampleFix":"// before\nparseAudioFxChain('{ \"version\": 1, \"effects\": [] }'); // missing `nodes`\n\n// after\nparseAudioFxChain('{ \"version\": 1, \"nodes\": [] }');","handlingStrategy":"validation","validationCode":"const parsed = JSON.parse(json);\nif (!Array.isArray(parsed?.nodes)) throw new Error('chain missing nodes array');","typeGuard":"function hasNodesArray(raw: unknown): raw is { nodes: unknown[] } {\n  return typeof raw === \"object\" && raw !== null && Array.isArray((raw as any).nodes);\n}","tryCatchPattern":"try { parseAudioFxChain(json); }\ncatch (err) { if (/missing a .nodes. array/.test(String(err))) { /* add nodes: [] */ } else throw err; }","preventionTips":["Always include a top-level nodes array (empty [] is valid for a pass-through chain).","Use serializeAudioFxChain to guarantee the schema."],"tags":["parsing","audio-fx-chain","validation","schema"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}