Skip to content

Handle errors with the NextPDF exception hierarchy

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.

Terminal window
composer require nextpdf/core:^3

You do not need any extra extension.

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\OracleConformanceFailure

This hierarchy has two practical consequences, both verified against the source:

  1. catch (NextPdfException $e) catches every exception under NextPDF\Exception, including the strict-mode violations. They all extend the abstract base.
  2. It does not catch everything the library can throw. NextPDF\Support\DegradedException extends RuntimeException directly, not NextPdfException. So a catch (NextPdfException $e) does not catch a degradation-policy rejection. To handle one, catch DegradedException (or the broader RuntimeException) 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.

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.

<?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());
}

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.

  • Order catch blocks specific → general. PHP matches the first compatible catch. A catch (NextPdfException $e) placed before catch (FontNotFoundException $e) makes the specific block dead code.
  • DegradedException is not a NextPdfException. Verified against the source, it extends RuntimeException. A single catch (NextPdfException $e) lets a strict-degradation rejection propagate. Catch it (or RuntimeException) 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) and getContext() 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 NextPdfException base type and instanceof, 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.

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.

  • 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.
StatementSpecClausereference_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.