Implement custom error recovery and retry strategies
At a glance
Section titled “At a glance”A production document service does more than catch and log an exception. It decides what to do next: continue with degraded output, switch to a second rendering path, retry with input the engine accepts, or deliver the pages built before the failure. This recipe shows four recovery strategies built on the NextPDF exception hierarchy and document state-inspection methods:
- Graceful degradation on a font failure — catch
NextPDF\Exception\FontNotFoundException, fall back to a guaranteed face, and keep building the document. - A fallback renderer — when the in-process
Document::writeHtml()path rejects input, retry throughDocument::writeHtmlChrome(), thenextpdf/artisanChrome bridge. - Retry with alternative HTML — when
NextPDF\Exception\HtmlParsingExceptionorNextPDF\Exception\CssResolutionBudgetExceededExceptionoccurs, retry with a simplified, known-good HTML variant. - Partial-document recovery — read
Document::getNumPages()after a failure, and save what was already built instead of discarding it.
You already know how to catch at the right level. The companion page Handle errors with the NextPDF exception hierarchy covers the hierarchy itself. This page shows what you do after the catch.
This recipe targets the open source software (OSS) core edition. Every
application programming interface (API) named here lives in nextpdf/core. The
only optional dependency is nextpdf/artisan for the Chrome fallback.
Install
Section titled “Install”composer require nextpdf/core:^3The fallback-renderer strategy additionally uses the Chrome bridge:
composer require nextpdf/artisanWhen nextpdf/artisan is absent, Document::writeHtmlChrome() throws
NextPDF\Exception\PageLayoutException instead of rendering. The fallback
strategy below treats a missing bridge as another recoverable case.
Conceptual overview
Section titled “Conceptual overview”Recovery depends on two facts about NextPDF, both verified against the source.
The exception hierarchy tells you what is recoverable. Every domain
exception extends the abstract base NextPDF\Exception\NextPdfException, which
extends RuntimeException and implements
NextPDF\Contracts\ContextAwareExceptionInterface. Catch a specific subtype to
choose a recovery path for that failure:
FontNotFoundExceptioncarriesgetFontName(),getSearchPaths(), andwasFallbackAttempted()— enough to retry with a different face.HtmlParsingExceptioncarriesgetRule(),getPosition(), andgetHtmlSnippet()— enough to decide whether a simplified retry is worth attempting.CssResolutionBudgetExceededExceptioncarriesgetVisits()andgetBudget()— a signal that a stripped-down stylesheet may clear a pathological selector.- One important boundary:
NextPDF\Support\DegradedExceptionextendsRuntimeExceptiondirectly, notNextPdfException. Socatch (NextPdfException $e)does not catch a degradation-policy rejection. When the activeNextPDF\Contracts\DegradationPolicyisStrictorBalanced, catchDegradedExceptionexplicitly to recover from it.
The document is inspectable while you build it. A Document exposes its
construction state through read-only accessors. getNumPages() returns the
total page count, including the active unflushed page, and getPage() returns
the zero-based index of the current page. After a mid-build failure, read
getNumPages() to learn whether any complete pages exist, then call save()
or getPdfData() to emit them. The engine also records non-fatal degradation
events: getWarnings() returns a list<NextPDF\Support\Warning>,
hasWarnings() reports whether any were collected, and hasDegradedParity()
reports whether output fidelity was affected. These methods let a recovery
routine distinguish “succeeded cleanly” from “succeeded with reduced fidelity”
without parsing an exception.
The degradation policy controls which events you handle as exceptions and which
you handle as warnings. NextPDF\Core\Config defaults to
DegradationPolicy::Balanced, which warns and continues for bounded
degradation but throws on a blocking impact. DegradationPolicy::Permissive
never throws and collects everything in the warning channel.
DegradationPolicy::Strict throws on any compliance-risk, semantic-loss, or
blocking impact. Choose the policy first, then write recovery for the failure
shapes that policy produces.
API surface
Section titled “API surface”The recovery code below uses these verified members:
NextPDF\Core\Document::createStandalone(?Config $config = null): self,addPage(),setFont(string $family, string $style = '', float $size = 12.0): static,cell(...),writeHtml(string $html): static,writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static,save(string $path): void,getPdfData(): string,getNumPages(): int,getPage(): int,getWarnings(): list<Warning>,hasWarnings(): bool,hasDegradedParity(): bool,addFontDirectory(string $directory): static.NextPDF\Core\Config::withDegradationPolicy(DegradationPolicy $policy): selfand thedegradationPolicydefault ofDegradationPolicy::Balanced.NextPDF\Contracts\DegradationPolicy—Strict,Balanced,Permissive.NextPDF\Exception\NextPdfException(abstract base),NextPDF\Exception\FontNotFoundException,NextPDF\Exception\HtmlParsingException,NextPDF\Exception\CssResolutionBudgetExceededException,NextPDF\Exception\WriterException,NextPDF\Exception\PageLayoutException.NextPDF\Support\DegradedException(carryingcapabilityandpolicy),NextPDF\Support\Capability(id,status,reason,isDegraded()),NextPDF\Support\Warning,NextPDF\Support\WarningSeverity.
Code sample — Quick start
Section titled “Code sample — Quick start”The smallest useful recovery catches a missing-font failure, falls back to a
guaranteed face, and continues. This snippet leaves out the broader handling
from the production sample. For a complete handler with logging and the
DegradedException boundary, read the production sample below.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\FontNotFoundException;
$doc = Document::createStandalone();$doc->addPage();
try { // A face that may not be installed on every host. $doc->setFont('CorporateSans', '', 12);} catch (FontNotFoundException $e) { // Recover: fall back to a face the engine always resolves. $doc->setFont('helvetica', '', 12);}
$doc->cell(0, 10, 'Rendered with a recovered font.', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');Code sample — Production
Section titled “Code sample — Production”The full example connects all four strategies in one render pipeline: a font
fallback, a renderer fallback from the in-process path to Chrome, an
alternative-HTML retry, and partial-document recovery driven by
getNumPages(). It honors the harness output channel and never catches a bare
Exception or leaves a catch block empty.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\ContextAwareExceptionInterface;use NextPDF\Contracts\DegradationPolicy;use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CssResolutionBudgetExceededException;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\HtmlParsingException;use NextPDF\Exception\NextPdfException;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;use NextPDF\Support\DegradedException;
/** * A minimal structured sink. In production this is your PSR-3 logger; the * exception class and its structured context become log fields. * * @param array<string, mixed> $context */function logRecovery(string $message, array $context): void{ fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");}
/** * Resolve a usable font, degrading from the requested face to a guaranteed * fallback. Returns the face actually applied so the caller can record it. * * @param non-empty-string $requested * @param non-empty-string $fallback * * @return non-empty-string */function applyFontWithFallback(Document $doc, string $requested, string $fallback): string{ try { $doc->setFont($requested, '', 12);
return $requested; } catch (FontNotFoundException $e) { // STRATEGY 1 — graceful degradation on a font failure. logRecovery('Font unavailable; degrading to a guaranteed face', [ 'exception' => $e::class, 'font_name' => $e->getFontName(), 'searched' => $e->getSearchPaths(), 'fallback' => $fallback, ]); $doc->setFont($fallback, '', 12);
return $fallback; }}
/** * Render HTML through the in-process pipeline, then through the Chrome bridge, * then through a simplified HTML variant. Each layer recovers a more specific * failure than the last. */function renderHtmlWithRecovery(Document $doc, string $primaryHtml, string $simplifiedHtml): void{ try { // Primary path: the in-process HTML/CSS pipeline. $doc->writeHtml($primaryHtml);
return; } catch (CssResolutionBudgetExceededException $e) { // STRATEGY 3 — retry with alternative HTML for a pathological selector. logRecovery('CSS resolution budget exceeded; retrying with simplified HTML', [ 'exception' => $e::class, 'visits' => $e->getVisits(), 'budget' => $e->getBudget(), ]); $doc->writeHtml($simplifiedHtml);
return; } catch (HtmlParsingException $e) { // STRATEGY 2 — fall back to the Chrome renderer for input the // in-process parser rejects. The Chrome bridge uses a browser CSS // engine, so it may accept what the in-process parser would not. logRecovery('In-process HTML parse failed; trying the Chrome fallback renderer', [ 'exception' => $e::class, 'rule' => $e->getRule(), 'position' => $e->getPosition(), ]);
try { $doc->writeHtmlChrome($primaryHtml);
return; } catch (PageLayoutException $chromeError) { // The Chrome bridge is absent (nextpdf/artisan not installed) or // rejected the input. Last resort: the simplified HTML variant // through the in-process pipeline. logRecovery('Chrome fallback unavailable; retrying with simplified HTML', [ 'exception' => $chromeError::class, ]); $doc->writeHtml($simplifiedHtml);
return; } }}
// --- Configure the degradation policy up front ---------------------------// Balanced (the default) warns on bounded degradation and throws only on a// blocking impact. A regulated workflow would choose DegradationPolicy::Strict.$config = (new Config())->withDegradationPolicy(DegradationPolicy::Balanced);$doc = Document::createStandalone($config);
$primaryHtml = '<h1>Quarterly report</h1><p>Body paragraph with rich styling.</p>';$simplifiedHtml = '<h1>Quarterly report</h1><p>Body paragraph (simplified).</p>';
$outputPath = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf';
try { $doc->addPage(); $applied = applyFontWithFallback($doc, 'CorporateSans', 'helvetica'); $doc->cell(0, 12, 'Custom error recovery patterns', newLine: true);
renderHtmlWithRecovery($doc, $primaryHtml, $simplifiedHtml);
$doc->save($outputPath);
logRecovery('Document built', [ 'font_applied' => $applied, 'pages' => $doc->getNumPages(), 'has_warnings' => $doc->hasWarnings(), 'degraded_parity' => $doc->hasDegradedParity(), ]);} catch (DegradedException $e) { // BOUNDARY: DegradedException extends RuntimeException directly, NOT // NextPdfException, so the catch-all below would not have caught it. // Under Strict/Balanced policy a blocking degradation lands here. logRecovery('Capability degraded under the active policy; emitting a built partial', [ 'exception' => $e::class, 'capability' => $e->capability->id, 'status' => $e->capability->status->value, 'reason' => $e->capability->reason ?? 'unknown', 'policy' => $e->policy->value, ]); // STRATEGY 4 — partial-document recovery: save whatever pages exist. if ($doc->getNumPages() > 0) { $doc->save($outputPath); }} catch (WriterException $e) { // Serialization or I/O failure: the in-memory document is valid but could // not be written. Surface the stage so infrastructure can act on it. logRecovery('PDF write failed; document was valid in memory', [ 'exception' => $e::class, 'writer_state' => $e->getWriterState(), 'output_path' => $e->getOutputPath(), ]);} catch (NextPdfException $e) { // Catch-all for every other NextPDF\Exception\*. STRATEGY 4 again: if any // complete pages were built before the failure, emit them rather than // discarding the work. $context = ['exception' => $e::class, 'pages' => $doc->getNumPages()]; if ($e instanceof ContextAwareExceptionInterface) { $context += $e->getContext(); } logRecovery('Unrecovered NextPDF failure; attempting a partial save', $context); if ($doc->getNumPages() > 0) { $doc->save($outputPath); }}
fwrite(STDERR, "Recovery pipeline complete.\n");STDOUT stays free for the harness. Recovery diagnostics go to STDERR, and
the Portable Document Format (PDF) file is written only to
NEXTPDF_COOKBOOK_OUTPUT.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Order catch blocks from specific to general. PHP matches the first
compatible
catch. Placingcatch (NextPdfException $e)beforecatch (WriterException $e)turns the specific block into dead code, becauseWriterExceptionextendsNextPdfException. DegradedExceptionsits outside the hierarchy. It extendsRuntimeException, notNextPdfException. A pipeline that catches onlyNextPdfExceptionlets a strict-policy rejection propagate uncaught. CatchDegradedException(or a broaderRuntimeException) when a non-default degradation policy is active.- A font fallback can fail too. If your fallback face is itself
unregistered, the second
setFont()throws again. Use a Base14 alias such ashelvetica, which the engine resolves without a filesystem lookup, or register a bundled face throughaddFontDirectory()at startup so the fallback is guaranteed. getNumPages()counts the active unflushed page. It returns the flushed page count plus one when a page is currently open. A “partial save” includes the page that was being built when the failure occurred, which is usually what you want. If you need only fully completed pages, branch ongetPage()as well.- The Chrome fallback changes fidelity, not only availability. The in-process pipeline and the Chrome bridge use different layout engines, so a document that falls back to Chrome can look different. Treat the fallback as a recovery, not a transparent substitute, and record which path produced the output.
- A retry must use known-good input. The simplified-HTML retry only helps
when the simplified variant is genuinely simpler: fewer nested selectors,
no
:has()chains that exhaust the resolution budget. Retrying with the same input that already failed loops to the same exception. - Inspect warnings after a clean run. A render that returns without
throwing can still have degraded. Check
hasDegradedParity()and readgetWarnings()before you treat the output as pixel-faithful; underDegradationPolicy::Permissiveevery degradation is a warning, never an exception.
Performance
Section titled “Performance”- Recovery adds cost only on the failure path. NextPDF throws on exceptional
states, so a clean render pays nothing for the surrounding
try/catch. - A renderer fallback re-runs the render. The in-process attempt is discarded and the Chrome attempt starts fresh, so a fallback render costs, in the worst case, both render times plus the inter-process round trip to Chrome. Budget for it when you set request timeouts.
- An alternative-HTML retry parses a second document. Keep the simplified variant small so the retry is cheap relative to the primary attempt.
- A partial save serializes the pages already built. Its cost scales with the surviving page count, not with the work that failed.
Security notes
Section titled “Security notes”- Do not show raw exception messages or filesystem paths to end users. A
FontNotFoundExceptionmessage includes the searched directories and aWriterExceptionincludes the output path; both leak server layout. Log the structured context server-side and return a generic message to the caller. - Treat retried HTML as untrusted input on every attempt. The fallback and the simplified-HTML retry both flow through the same input boundary; the in-process pipeline and the Chrome bridge each apply their own HTML security policy, and a retry does not relax that validation. Do not assume a “simplified” variant is safer because you authored it.
- A partial save still writes a file. Apply the same path validation,
permissions, and storage-location rules to a partial output that you apply to
a complete one.
Document::save()rejects stream wrappers and null bytes and resolves the parent directory to block path traversal, but the destination you pass is your responsibility.
Conformance
Section titled “Conformance”This recipe makes no normative standards claim. It composes the public NextPDF
exception and document-inspection APIs into recovery control flow; it does not
assert behavior defined by ISO 32000-2 or any other standard, so it carries no
citations: block.
This page is verified with the semantic reproducibility profile. The
recovered document carries a trailer /ID and a modification date that are
regenerated per save, so byte identity is not achievable. The structural
abstract syntax tree (AST) plus metadata-only comparison is stable across runs.
See also
Section titled “See also”- Handle errors with the NextPDF exception hierarchy — catch granularity and structured context, the foundation this page builds on.
- Exception module — the full exception reference.
- Support module —
DegradedException,Capability,Warning, and the degradation types. - Config module — degradation-policy configuration.
- Render PDFs safely in a long-running worker — recovery in a worker that reuses shared registries.