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
- Repair the PDF with 'mutool clean in.pdf out.pdf' or 'qpdf --linearize' to rebuild a clean, acyclic page tree.
- If you produce PDFs, ensure /Pages /Kids never references an ancestor (validate the tree before writing).
- Load with ignoreErrors/recovery and accept that the cyclic page may be skipped.
- 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
- Repair cyclic page trees with qpdf/mutool before loading.
- When producing PDFs, never let a /Pages /Kids reference an ancestor.
- Run 'qpdf --check' to detect cycles pre-runtime.
- Enable ignoreErrors so PDF.js can attempt recovery.
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
- Page dictionary kid reference points to wrong type of object
- Page dictionary kids object is not an array.
- Page index ${pageIndex} not found.
- The reference does not point to a /Page dictionary.
- Node must be a dictionary.
AI-assisted analysis of mozilla/pdf.js@5903d58d58 (2026-08-13).
Data as JSON: /api/errors/ebd13219d9a8f1d6.
Report an issue: GitHub.