← dompdf.js Studio

Error Handling and Debugging: Making PDF Export Rock-Solid

The worst thing that can happen after a PDF export feature ships is users reporting that clicking export does nothing, or that the PDF content is wrong. Unlike a backend, where logs are available, browser-side failures happen on users' devices: hard to investigate, long feedback loops, and no server logs to read. Error handling must therefore be designed in advance: anticipate failure scenarios, catch exceptions uniformly, and give users a concrete fallback. The failure surface of dompdf.js concentrates in a few areas: content parsing problems, resource loading failures for images and fonts, render-stage exceptions, and downloads blocked by the browser. This guide systematically covers the causes and recognition of each failure class, shows a complete try/catch wrapper pattern with production-grade error handling code, and introduces minimal reproduction and staged verification as efficient debugging techniques. We also cover the deterministic-versus-transient distinction that tells you whether to fix or to retry, and the monitoring habits that catch regressions before users do. The goal is a complete system that prevents, catches, and recovers from errors — so export stops being a source of fear and becomes a reliable feature. We also cover a small error taxonomy that turns ad-hoc handling into a stable protocol, plus structured logging that turns production diagnosis into a routine query.

The Failure Landscape: Where Export Can Go Wrong

The first class is content problems: incomplete HTML template syntax, styles depending on external resources, or dynamic data concatenation errors. These usually surface at render time as missing output, broken layouts, or a thrown exception. The preventive measure is to validate templates independently and to type-check and null-check data before it enters the template, intercepting errors before they reach the renderer.

The second class is resource problems: cross-origin or dead image URLs, font loading failures, and external CSS that never arrives. Resource problems are the most deceptive — the page looks fine while the PDF silently misses images or glyphs. The robust strategy is to wait for all resources before exporting, or to inline critical resources into the template so rendering no longer depends on the external environment at all.

The third class is environment problems: browser security policies blocking downloads, memory pressure interrupting rendering, and compatibility differences across browsers. These cannot be fully eliminated in code, but fallbacks contain the damage — prompt the user to refresh and retry, or offer backend rendering as an alternative path. The core function stays available no matter which environment fails.

The universal recognition technique: catch exceptions in try/catch, record message and stack, and log at every critical export node. With logs in place, a user report of 'export failed' immediately tells you which layer broke instead of inviting guesswork. Investigation time drops by an order of magnitude, and the accumulated cases become team knowledge rather than tribal lore.

It is also worth distinguishing between deterministic and transient failures. Template bugs are deterministic — they fail the same way every time and are fixable by inspection. Resource and environment failures are transient — they come and go with network and browser state and are best handled by retry and fallback logic. Classifying the failure before fixing it prevents both over-engineering and repeated whack-a-mole.

One more class is easy to miss: logical errors where the template and resources are fine but the output does not match expectations — wrong data order, missing fields. These never throw, so the only way to catch them is result validation. Assert on page count and key fields after export and fold logical errors into the same handling system, rather than letting them pass silently.

Building a Unified Exception Boundary with try/catch

Wrapping the entire export flow in try/catch is the first line of defense. The catch block should do three things: record the error details for investigation, inform the user for experience, and execute a fallback for recovery. All three are necessary — a bare 'export failed' toast tells users nothing about what to do and leaves the problem unresolved.

Make error messages specific: distinguish content errors, resource loading failures, and blocked downloads, and pair each with concrete advice — check the data, retry the export, or switch browsers. The more explicit the guidance, the less users need support, the fewer tickets arrive, and the better the product reputation becomes. Message quality is the most underrated part of error handling.

Remember that try/catch must wrap the whole async flow, including the save call that triggers the download. When rendering succeeds but the download is blocked, the exception still needs to be caught and the user guided to retry manually — otherwise you hit the awkward state where the PDF was generated but the user never received the file. These half-success states generate the most complaints, so give them a dedicated branch.

One refinement that pays off: define a small error taxonomy with codes ('CONTENT_ERROR', 'RESOURCE_ERROR', 'DOWNLOAD_BLOCKED') and map exceptions to codes at the boundary. The UI layer then switches on codes instead of parsing message strings, tests can assert on codes, and the whole error system stays stable as messages get rewritten for tone or language.

Structured logging is the natural companion: assemble error type, stage, and context into a fixed-format object before reporting, so backend search and aggregation work cleanly. Compared with console.log statements scattered across the code, structured logs are the difference between archaeology and a routine query when production issues arrive.

Putting the taxonomy to work, a small example: define an ERROR_CODES constant mapping each failure class to a stable string — CONTENT_ERROR, RESOURCE_ERROR, DOWNLOAD_BLOCKED, UNKNOWN_ERROR — and classify exceptions at the boundary with a dedicated function. The UI switches on the code, the logger records the code, and analytics aggregates by code. Because codes are stable strings, rewriting a user-facing message never breaks consumers, and tests assert on codes rather than fragile message text. The taxonomy turns ad-hoc error handling into a small, consistent protocol between the export layer and the rest of the application — and protocols are what make systems maintainable as they grow.

Production-Grade Error Handling Example

This example packages the complete flow: disable the button to prevent duplicate triggers, run export inside try/catch, map exceptions to friendly messages, and always restore the UI in a finally block. The friendlyMessage function is where the error taxonomy lives, so refining messages later touches one function only.

import { DomPDF } from 'dompdf.js';

async function safeExport(container, filename) {
  const btn = document.querySelector('#exportBtn');
  btn.disabled = true;                 // prevent duplicate triggers
  setStatus('Generating PDF…');

  try {
    const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
    pdf.addPage(container);            // collect content
    pdf.save(filename);                // render + download
    setStatus('Export complete');
  } catch (err) {
    console.error('[PDF export failed]', err);
    setStatus('Export failed: ' + friendlyMessage(err));
    btn.disabled = false;              // fallback: offer a retry
  } finally {
    setTimeout(() => setStatus(''), 3000);
  }
}

function friendlyMessage(err) {
  if (err instanceof TypeError) return 'Content parse error, please check the template';
  if (String(err).includes('download')) return 'Download was blocked, please click retry';
  return 'Unknown error, please try again later';
}

Debugging Techniques: Minimal Reproduction and Staged Verification

When rendered output is wrong, start with a minimal reproduction: trim the template down to just the offending element, replace the data with fixed values, and export from a standalone page. Minimal reproduction minimizes variables — a few lines of code will tell you whether the problem lives in the template, the data, or the engine — which beats guessing inside the full application and gives you a clean case to paste into a GitHub issue or community question.

Step two is staged verification: print the content passed to addPage and confirm the input is correct; check that images and fonts are ready; only then investigate the render stage. Each step has a clear pass or fail signal, so the failing step is identified at a glance and the whole investigation rarely takes more than a few minutes. The discipline of checking input before suspecting the engine prevents the most common debugging dead end.

Step three is version comparison: if an anomaly appeared after upgrading dompdf.js, run the same content against the old version to determine whether it is a regression. Save the minimal reproduction cases from before each upgrade as a regression test set; every future upgrade can then be verified automatically, and the risk of shipping a breaking change drops dramatically.

One more trick: log the template string to the console, copy it into a local HTML file, and view it directly in the browser. Browser rendering of the HTML closely matches the PDF output, so getting the HTML right in the browser first — then investigating only the PDF-specific differences — filters out the majority of template-level errors and leaves a small, tractable remainder.

And when you do find the bug, resist the urge to patch around it. A fix in the template or data layer addresses the root cause; a fix that special-cases the symptom will resurface the next time the data changes. Root-cause fixes compound, symptom patches compound in the wrong direction.

Reproduction speed is debugging speed: freeze each minimal reproduction as a runnable HTML file in your examples or tests directory, and the next time a similar issue appears you open it and run it directly. Rewriting reproduction cases by hand is the most time-wasting form of debugging there is; a one-time investment keeps paying off for years.

Diagnosing Image and Font Failures

Image failures typically show up as missing images or placeholders in the PDF. The diagnostic order: open the image URL directly in the browser to confirm it is reachable; check the cross-origin (CORS) configuration, since cross-origin images require the server to allow them; finally check the format, since the PDF engine supports a finite set of formats and unsupported ones should be converted in advance. Three checks cover the overwhelming majority of cases.

The most reliable image strategy is base64 inlining: convert the image to a Data URL and include it in the template, so rendering no longer depends on the network or CORS at all, and timing becomes fully controllable. The cost is a larger template string, but for critical documents (contracts, invoices) the trade is clearly worth it — better a slightly larger file than a rendering failure in a legally significant document.

Font problems present as garbled text or tofu blocks. dompdf.js bundles Source Han Sans SC, so Chinese text works out of the box; for custom fonts, supply the font file bytes through the officially documented font configuration interface. When diagnosing, first confirm the content is UTF-8 encoded, then confirm the font resource loaded — two steps cover nearly all font anomalies, and the engine should rarely be your first suspect.

Finally, run a resource self-check before export: iterate the images and fonts referenced by the template and confirm each is ready, alerting early rather than discovering the gap after rendering. The self-check is small but intercepts a large share of silent failures, and in batch scenarios its value multiplies — one broken resource would otherwise poison every document in the run.

A pragmatic policy: distinguish hard dependencies (contract seals, invoice QR codes) from soft decorations (backgrounds, decorative icons). Hard dependencies get the full self-check and inlining treatment; soft decorations can degrade gracefully. Spending the robustness budget where the document's validity depends on it is the efficient allocation.

Add one more dimension to the self-check: dimensions and size. An image that loads fine can still be so large or so wide that it slows rendering and bloats the file. Validate image dimensions during the self-check and prompt for compression above a threshold, intercepting performance problems before they reach the export — two categories of failure caught by one check.

Production Best Practices for Error Handling

Before launch, run the export flow through a full scenario matrix: normal content, empty data, extremely long text, missing images, and blocked downloads — and confirm the error message and fallback behavior in every scenario. Capture these scenarios as test cases so every template change or library upgrade can be regression-tested, preventing the classic cycle of fixing one bug and introducing another.

Attach context to logs: filename, page count, content source identifier, and browser version. When a production user reports a problem, these details reconstruct the scene quickly; keep export logs in their own category so they can be searched by time and user instead of drowning in business logs. The separation turns log review from archaeology into a routine query.

Finally, build a fallback matrix: render failure prompts a retry; resource failure prompts a data check; blocked download guides the user to manual download; persistent failure falls back to backend rendering or an export report. Every failure class has an explicit exit, so users are never stuck in a 'failed' state with nowhere to go. Reliability reputation is built exactly this way — one well-handled failure at a time.

And measure what matters: track the export success rate in your analytics. A falling success rate is the earliest signal of a regression, long before users complain loudly enough to reach you. Instrumenting the happy path and the failure path with equal care gives you both the diagnosis and the early warning system for your export feature — a lightweight success counter per version and browser dimension turns the export pipeline into a monitored service.

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。