Handle errors with the NextPDF exception hierarchy
At a glance
Section titled “At a glance”NextPDF throws typed exceptions for exceptional states. It never hides an
error behind a false or null return. Every domain exception extends the
same abstract base, NextPdfException, and exposes structured diagnostic
context through ContextAwareExceptionInterface. This recipe shows you where
to catch, and how to log structured context for an application performance
monitoring (APM) pipeline. It also shows which failures the single catch-all
does not cover.
Install
Section titled “Install”composer require nextpdf/core:^3You do not need any extra extension.
Conceptual overview
Section titled “Conceptual overview”The hierarchy is:
RuntimeException └── NextPdfException (abstract, implements ContextAwareExceptionInterface) ├── InvalidConfigException ├── FontNotFoundException ├── FontParsingException ├── ImageProcessingException ├── WriterException ├── SignatureException ├── EncryptionException ├── HtmlParsingException ├── … (every domain exception under NextPDF\Exception) └── Strict\StrictModeViolation (abstract) ├── Strict\IncompatibleRenderingModeException └── Strict\OracleConformanceFailureThis hierarchy has two practical consequences, both verified against the source:
catch (NextPdfException $e)catches every exception underNextPDF\Exception, including the strict-mode violations. They all extend the abstract base.- It does not catch everything the library can throw.
NextPDF\Support\DegradedExceptionextendsRuntimeExceptiondirectly, notNextPdfException. So acatch (NextPdfException $e)does not catch a degradation-policy rejection. To handle one, catchDegradedException(or the broaderRuntimeException) explicitly. This recipe makes that boundary explicit instead of treating one catch-all as complete coverage.
NextPdfException::getContext() returns an array<string, mixed> with
snake_case keys and only primitive values, or lists of primitive values. You
can serialize it directly into a PSR-3 logger’s context array. PSR-3 §1.3
places an exception under the 'exception' context key. NextPDF’s
getContext() adds domain detail beside that key, not the exception object
itself.
API surface
Section titled “API surface”This API surface comes from the PHPDoc on
NextPDF\Exception\NextPdfException,
NextPDF\Contracts\ContextAwareExceptionInterface, the concrete domain
exceptions (for example NextPDF\Exception\FontNotFoundException, with
getFontName() / getSearchPaths() / wasFallbackAttempted()), and
NextPDF\Support\DegradedException (which carries the Capability and
DegradationPolicy). The examples below use
NextPdfException::getContext() and the per-exception accessors.
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;
try { $doc = Document::createStandalone(); $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, 'Hello'); $doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');} catch (NextPdfException $e) { // Every NextPDF\Exception\* (and strict-mode violation) lands here. // $e->getContext() is APM-safe structured detail. error_log($e->getMessage());}Code sample — Production
Section titled “Code sample — Production”The complete example shows granular catches, structured-context logging, and
the DegradedException boundary. It also preserves the harness output
channel.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Contracts\ContextAwareExceptionInterface;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\NextPdfException;use NextPDF\Support\DegradedException;
/** * A minimal PSR-3-shaped sink. In production this is your real logger; * the exception goes under the 'exception' key (PSR-3 §1.3) and the * NextPDF structured context is merged in as domain detail. * * @param array<string, mixed> $context */function logError(string $message, array $context): void{ fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");}
$doc = Document::createStandalone();$doc->setTitle('Exception handling patterns');
try { $doc->addPage(); $doc->setFont('helvetica', 'B', 16); $doc->cell(0, 12, 'Exception-aware error handling', newLine: true);
// This call succeeds; the catch blocks below show the SHAPE of handling. $doc->setFont('helvetica', '', 11); $doc->cell(0, 8, 'Catch specifically, then fall back to the base.', newLine: true);} catch (FontNotFoundException $e) { // Most specific first: actionable, typed accessors. logError('Font missing — using a fallback face', [ 'exception' => $e::class, 'font_name' => $e->getFontName(), 'searched' => $e->getSearchPaths(), 'fallback' => $e->wasFallbackAttempted(), ]);} catch (NextPdfException $e) { // Catch-all for every NextPDF\Exception\* including strict violations. $context = ['exception' => $e::class]; if ($e instanceof ContextAwareExceptionInterface) { $context += $e->getContext(); } logError($e->getMessage(), $context);} catch (DegradedException $e) { // BOUNDARY: DegradedException extends RuntimeException directly, NOT // NextPdfException. The catch above would NOT have caught it. This // explicit block (or a broader RuntimeException) is required. logError('Capability degraded under the active policy', [ 'exception' => $e::class, 'capability' => $e->capability->id, 'policy' => $e->policy->value, ]);}
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
fwrite(STDERR, "Document built; handlers wired.\n");STDOUT stays free for the harness; the PDF goes only to
NEXTPDF_COOKBOOK_OUTPUT.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Order catch blocks specific → general. PHP matches the first
compatible
catch. Acatch (NextPdfException $e)placed beforecatch (FontNotFoundException $e)makes the specific block dead code. DegradedExceptionis not aNextPdfException. Verified against the source, it extendsRuntimeException. A singlecatch (NextPdfException $e)lets a strict-degradation rejection propagate. Catch it (orRuntimeException) explicitly when degradation policy is in play.getContext()is APM-safe by contract. Keys are snake_case. Values are primitives or lists of primitives, with no nested objects and no resources. You can serialize it directly. It never contains document bytes.- Do not parse exception messages. Messages are human-readable and can
change. Use the typed accessors (
getFontName(),capability->id, and so on) andgetContext()as the stable machine surface. - Stale count caveat. Older material may cite a fixed “N domain
exceptions”. The hierarchy grows across releases. Rely on the
NextPdfExceptionbase type andinstanceof, never on a hard-coded count. - PSR-3 placeholders stay strings. When you log, keep the message a
string with
{placeholder}tokens, and put values in the context array (PSR-3 §1.2). Do not interpolate the exception object into the message.
Performance
Section titled “Performance”Exception handling adds no steady-state cost. NextPDF throws only for
exceptional states, and getContext() builds a small array on demand. The
performance_budget (wall_ms: 2000, peak_mb: 96) bounds the harness run
for this recipe, not arbitrary documents.
Security notes
Section titled “Security notes”getContext()is designed to be log-safe: primitives only, no document payload, and no file bytes. You are still responsible for the values you add to a log context. Scrub anything user-supplied (a file path, for example) under your logging policy before it reaches a sink.- Do not echo raw exception messages to end users in a way that leaks the filesystem layout. Show a generic message, and log the structured context server-side.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
An exception belongs in the PSR-3 log context under the exception key. | PSR-3 | §1.3 | |
| Log messages stay strings; placeholder names map to context keys. | PSR-3 | §1.2 | |
| The modification date is regenerated per save, so output is structurally (not byte) stable. | ISO 32000-2 | §14.3 |
This recipe is verified with the structural reproducibility profile. The
output carries a trailer /ID and a modification date that are regenerated on
every save, so byte identity is not achievable. The qpdf-normalised structure
stays stable.