Migrating pdf.js 3 to 5: The Breaking Changes Nobody Warns You About
We moved 26 PDF tools from pdf.js 3.11.174 to 5.5.207 to fix CVE-2024-4367. Along the way we hit an ESM change that silently sets no global, a property that cannot be assigned to, and a removed option that turned out to change nothing. Here are the measurements and the code that works.
If you want better results with pdf.js migration breaking changes, this guide explains the practical steps, common mistakes, and useful browser-based tools that make the process easier.
We had 26 browser tools running on pdf.js 3.11.174. That version predates the fix for CVE-2024-4367, a same-origin code execution flaw triggered by a crafted PDF, so the upgrade was not optional.
What we expected was a version bump. What we got was three failures that every static check in our pipeline passed straight through, one of which shipped and broke every PDF tool we have.
Quick Takeaways
Focus first on why we targeted 5.5.207 and not the latest.
Apply the steps from this guide to improve pdf.js migration breaking changes without overcomplicating the workflow.
Use PDF to Word to turn this advice into action directly in your browser.
Common questions readers ask about this topic and the tools connected to it.
We moved 26 PDF tools from pdf.js 3.11.174 to 5.5.207 to fix CVE-2024-4367. Along the way we hit an ESM change that silently sets no global, a property that cannot be assigned to, and a removed option that turned out to change nothing. Here are the measurements and the code that works.
This article is useful for readers who want practical developer guidance and a clearer way to apply the advice using free browser-based tools.
Yes. This article connects naturally with ToolsMonk tools such as PDF to Word, PDF to PowerPoint, PDF Metadata Editor, Merge PDF, so readers can move from learning to doing in the same workflow.
The Developer Desk is the engineering team that builds ToolsMonk's developer utilities, JSON, regex, encoding, hashing, formatters, and converters. These guides are written by the engineers who implement the tools, so the explanations of formats, algorithms, and edge cases come from building them, not just describing them. Every guide is researched, written, and reviewed by the same team that designs and maintains the underlying ToolsMonk tools, then fact-checked against primary sources and updated as standards change.
This is the full account: which version to target and why, the three traps, and the measurements we used to prove the migration changed nothing about output fidelity.
Why We Targeted 5.5.207 and Not the Latest
The CVE is fixed in 4.2.67 and every version above it. That means the security argument is satisfied by 4.2.67, 5.x and 6.x equally, and the choice between them is purely about API surface.
We checked the shipped type definitions rather than the release notes, because release notes summarise and typings are the contract.
PDFDocumentProxy.destroy() exists in 3.x and 5.x and is REMOVED in 6.x. We had 52 call sites across 29 files. In 6.x the teardown moves to the loading task: keep the object returned by getDocument and call task.destroy().
isEvalSupported exists in 3.x and 5.x and is gone in 6.x, along with the DocumentInitParameters entry for it.
SVGGraphics is gone from 4.x onward. No CVE-fixed version has it.
disableCombineTextItems is gone from 4.x onward.
getStructTree, getMarkInfo and getPermissions are present throughout.
So 5.5.207 fixes the CVE at the cost of two removals, and 6.2.108 fixes the same CVE at the cost of four plus 52 call sites. Since the security outcome is identical, the smaller migration wins.
Upgrading to the newest available version is a habit, not a requirement.
Pro Tip
Check the removals against your own call sites before picking a target version. A recursive grep for doc.destroy took thirty seconds and decided the whole plan.
Trap 1: The Modern Build Is ESM, and a Script Tag Sets No Global
If you would rather do this step in the browser than by hand, PDF to PowerPoint handles it without a signup.
pdf.js 3.x could be injected as a classic script tag, after which the library appeared at window.pdfjsLib. From 4.x the modern build is an ES module.
Load it with a script tag and it does not error: the file downloads, the tag fires its load event, and no global is ever created. The calling code then waits forever for a library that has already arrived.
The fix is a dynamic import, with bundler-ignore comments so your bundler leaves the URL alone at build time:
Stop reading window.pdfjsLib. It does not exist any more. Use the module namespace the import resolves to.
Pin the exact version in the URL. A major-only pin auto-upgrades you into the next set of breaking changes.
Trap 2: You Cannot Assign to getDocument
This is the one that shipped and broke production, and it is worth understanding precisely because the code reads as obviously correct.
We wrap getDocument at a single choke point so that every call gets isEvalSupported: false, which is defence in depth against the very class of bug the CVE belongs to. The natural way to write that wrapper is to replace the property:
lib.getDocument = wrapped throws TypeError: Cannot set property getDocument of #<Object> which has only a getter.
Measured in a real browser, getDocument on the namespace is an accessor with a getter, no setter, and configurable: false. Module namespace objects have the same immutability.
Both approaches are therefore impossible, and in strict mode, which every module is, they throw rather than failing quietly.
Warning
The consequence was worse than a broken wrapper.
The throw escaped inside a load handler, so the promise the tools were awaiting never settled and never rejected.
Users saw a raw TypeError in the tool's own error banner, and separately the hardening flag was never applied,
so the mitigation existed in the source and was completely inert.
Every static gate we have passed on that code.
The fix is a Proxy. The namespace is never mutated; the get trap returns the wrapped function:
Wrap the namespace in a Proxy whose get trap intercepts only getDocument and passes everything else through.
Spread the caller's source object FIRST, then set your flag, so a call site cannot accidentally override the hardening back on.
Wrap any load handler body in try/catch that REJECTS the promise. A throw inside an onload handler cannot reject a promise by itself; it escapes uncaught and the promise simply never settles. A hang is worse than an error.
Trap 3: A Removed Option That Changed Nothing
PDF Metadata Editor is the quickest way to apply what this section describes to your own file.
Losing disableCombineTextItems looked like the biggest accuracy risk in the migration.
Our PDF to Word converter detects table columns from the x-position of each text item, and its own source comments warned that merged text runs destroy that geometry.
So we loaded both engines into one page and ran the same table-bearing document through each, comparing the extracted text item by item.
3.11.174 with disableCombineTextItems: true gave 51 text items.
The option disappeared because the behaviour it disabled disappeared with it. pdf.js 4.0 stopped merging text items by default, which made the flag redundant.
A removed option is not automatically a regression; sometimes it means the thing you were opting out of is gone.
Proving the Migration Did Not Change the Output
Type checks and unit tests tell you the code compiles and that your assertions still hold. They cannot tell you whether a converted document still looks right. We drove real conversions end to end:
PDF to Word produced a 36.7 KB DOCX containing 274 characters with every table cell in the correct row and column order.
PDF to PowerPoint produced 26 editable text boxes, matching the 26 discrete text runs in the source document.
PDF Metadata Editor read the real document title and rendered a page thumbnail.
Console errors from pdf.js across all of it: zero.
Warning
SVGGraphics is gone and no CVE-fixed version has it.
If you use it, your fallback path is now your only path.
Ours was already guarded by a typeof check and silently switched to redrawn tiles, which worked on the test document.
We have not verified it against gradients, clipping or complex vector art, and we say so rather than claiming a clean migration.
One More If You Go to 6.x
Worth keeping open alongside this guide: Merge PDF, which covers the same job in a couple of clicks.
We keep pdfjs-dist as a devDependency for tests. When Dependabot moved it to 6.2.108, three integration tests failed immediately with TypeError: doc.destroy is not a function.
That is the removal listed at the top of this article, arriving in the test suite rather than in production, because our runtime loads pdf.js from a CDN at a pinned version and the package is only used by tests.
Pro Tip
If your app loads pdf.js from a CDN but you also depend on the npm package for tests, those two versions can drift a whole major apart without anything failing.
Worth an assertion that pins them together, or at least a comment explaining that they are deliberately independent.
The Checklist
Pick the lowest version that fixes the CVE and keeps your API. Newest is not free.
Read the shipped .d.ts files, not the changelog, for what was removed.
Switch script-tag injection to dynamic import, and stop reading window.pdfjsLib.
Wrap the namespace with a Proxy. Do not assign to its properties.
Make load handlers reject on throw, so a failure is an error rather than a hang.
Verify fidelity by converting a real document and inspecting the output, not by running the test suite.
Our PDF tools run pdf.js in your browser, so the document is processed on your own device. You can try the converted output yourself with the PDF to Word and PDF to PowerPoint tools.