mozilla/pdf.js · error · FormatError

Pages tree contains circular reference.

Error message

Pages tree contains circular reference.

What it means

Thrown in Catalog.getPageDict() (catalog.js:1343) when the top-down traversal of the /Pages tree encounters an indirect Ref that is already in the visitedNodes RefSet — i.e., the page tree contains a cycle. PDF page trees must be acyclic; a loop would hang the traversal, so PDF.js detects and rejects it.

Source

Thrown at src/core/catalog.js:1343

    const xref = this.xref,
      pageKidsCountCache = this.pageKidsCountCache,
      pageIndexCache = this.pageIndexCache,
      pageDictCache = this.pageDictCache;
    let currentPageIndex = 0;

    while (nodesToVisit.length) {
      const currentNode = nodesToVisit.pop();

      if (currentNode instanceof Ref) {
        const count = pageKidsCountCache.get(currentNode);
        // Skip nodes where the page can't be.
        if (count >= 0 && currentPageIndex + count <= pageIndex) {
          currentPageIndex += count;
          continue;
        }
        // Prevent circular references in the /Pages tree.
        if (visitedNodes.has(currentNode)) {
          throw new FormatError("Pages tree contains circular reference.");
        }
        visitedNodes.put(currentNode);

        const obj = await (pageDictCache.get(currentNode) ||
          xref.fetchAsync(currentNode));
        if (obj instanceof Dict) {
          let type = obj.getRaw("Type");
          if (type instanceof Ref) {
            type = await xref.fetchAsync(type);
          }
          if (isName(type, "Page") || !obj.has("Kids")) {
            // Cache the Page reference, since it can *greatly* improve
            // performance by reducing redundant lookups in long documents
            // where all nodes are found at *one* level of the tree.
            if (!pageKidsCountCache.has(currentNode)) {
              pageKidsCountCache.put(currentNode, 1);
            }
            // Help improve performance of the `getPageIndex` method.

View on GitHub (pinned to 5903d58d58)

Solutions

  1. Repair the PDF with 'mutool clean in.pdf out.pdf' or 'qpdf --linearize' to rebuild a clean, acyclic page tree.
  2. If you produce PDFs, ensure /Pages /Kids never references an ancestor (validate the tree before writing).
  3. Load with ignoreErrors/recovery and accept that the cyclic page may be skipped.
  4. Fall back to pdfDocument.getAllPageDicts (the recovery-mode eager traversal) if available, which tolerates more breakage.
Defensive patterns

Strategy: try-catch

Validate before calling

// You cannot pre-walk the page tree via public API cheaply.
// Best prevention: repair known-cyclic PDFs before loading.
// qpdf --linearize in.pdf out.pdf  (rebuilds page tree)

Try / catch

try {
  const page = await pdf.getPage(n);
} catch (err) {
  if (/circular reference/i.test(err?.message)) {
    // try recovery traversal or report the broken page tree
    console.warn('Page tree has a cycle; document needs repair', err);
  } else throw err;
}

Prevention

When it happens

Trigger: pdfDocument.getPage(n) triggers getPageDict, which walks Kids references; a node's Kids array (directly or transitively) points back to an ancestor Ref already in visitedNodes. Common in malformed PDFs where a /Pages node lists itself or a parent as a child.

Common situations: A buggy PDF producer that wrote a self-referential or cyclic Kids array; a corrupt page tree after a bad merge/split operation; an edited PDF with duplicated refs.

Related errors


AI-assisted analysis of mozilla/pdf.js@5903d58d58 (2026-08-13). Data as JSON: /api/errors/ebd13219d9a8f1d6. Report an issue: GitHub.