Skip to content

Errors as a feature

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF treats its exception hierarchy as an API surface, designed with the same care as the methods that throw it. A failure is specific, typed, catchable at the granularity you need, and carries structured context for your logs.

This page shows that surface in the engine’s own source: the base type, the typed subclasses, the named constructors that bind a root cause to the message, and the structured context every NextPDF exception exposes.

An error message is the engine speaking to you at the worst possible moment: production, 2 a.m., and a document that should have shipped. What that message says then decides whether the next step is a fix or a long investigation.

A generic RuntimeException: something went wrong gives you nowhere to go. It tells you that the engine failed, but not what failed, not where, and certainly not what to do. Human-factors guidance is direct about this. An error should explain itself well enough that fixing it is the obvious next step, not a research project ( Spec: ISO 9241-110, §5.6.4.3 ). An exception that names the cause and the remedy is not a nicety. It is the difference between a five-minute fix and a five-hour one.

  • Every NextPDF failure extends one abstract base, NextPdfException, so you can catch all library errors with a single type.
  • Below it sit specific, typed subclasses — a font that cannot be found, a config that is invalid, a signature operation that failed — so you can catch exactly the failure you can handle.
  • Every NextPDF exception implements ContextAwareExceptionInterface and exposes getContext(): a structured, log-safe map, so you never parse a message string to recover diagnostics.
  • Messages are actionable: named constructors bind the actual root cause (and often the fix) to the message, instead of a generic template.
  • Each exception class documents who can act on it — developer, infrastructure, or library caller — so triage starts before you read the stack trace.

The hierarchy is shallow and deliberate. There is one base, a layer of domain-specific types, and a contract every one of them follows.

One base, catch-all by design. NextPdfException is abstract, extends RuntimeException, and implements ContextAwareExceptionInterface:

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

Abstract is a decision. You never catch the vague base by accident, because it is never thrown directly. You catch it deliberately, as a backstop, and you catch a specific subclass when you can do something specific.

Specific, typed subclasses. A missing font is not a generic error; it is FontNotFoundException, and it carries the data you need to act:

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

The message names the font and the exact paths searched. You do not guess which directory was missing; the exception tells you.

Structured context, not string-scraping. Every exception returns a snake_case, primitives-only map that is safe to serialize straight into a log or an APM payload:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

The contract is explicit about why. A logging middleware can call $logger->error($e->getMessage(), $e->getContext()) for any NextPDF exception without ever parsing the message. The message is for humans. The context is for machines. Neither has to act as the other.

Actionable messages via named constructors. This is where errors stop being incidental and become designed. SignatureException does not only say “signing failed at level B-LT”. It offers named constructors that bind the real root cause, and frequently the exact remedy, to the message:

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

The message states what is wrong and what to do about it. There are sibling constructors for a missing capability package, an absent HTTP client, a digest-only algorithm chosen by mistake, a key type that does not match the algorithm, and more. Each one turns a class of failure into a sentence a developer can act on without reading the engine’s source.

Failures that are loud on purpose. Some exceptions exist precisely so a silent gap becomes a noisy one. NotImplementedException carries a machine-grep-able feature label and a followUp reference:

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

A reached-but-unwired path throws this instead of returning a plausible no-op. The same idea drives StrictModeViolation, whose subclasses carry a short grep-able label for the deviating construct plus optional location and citation context. A spec deviation becomes a typed, contextual stop, not a quietly wrong render.

Triage metadata in the class itself. Each exception class names who can act on it in its docblock. For example, FontNotFoundException is “Developer (verify font path) or Infrastructure (fix file permissions)”. InvalidConfigException is “Developer (fix configuration before calling NextPDF)”. NotImplementedException is “Library callers — either remove the call or pin to a future release”. Triage starts before the stack trace, because the question “is this mine or operations?” already has an answer written down.

The table summarises the design and what each property buys you.

Design propertyIn the sourceWhat it buys you
One abstract baseNextPdfException (abstract, implements the context interface)Catch every library error with one type, never the vague base by accident
Specific typed subclassesFontNotFoundException, InvalidConfigException, SignatureException, …Catch exactly the failure you can handle
Structured contextgetContext() — snake_case primitives onlyLog or ship to APM without parsing a message string
Actionable messagesNamed constructors bind root cause + remedyA sentence you can act on, not a template
Loud-on-purposeNotImplementedException, StrictModeViolationA silent gap becomes a typed, grep-able stop
Triage metadata”Actionable by:” in each class docblockKnow whose problem it is before reading the trace

This page is Evidence: Code-backed : every class, signature, and message shape is quoted from the engine’s exception namespace, not paraphrased.

  • The abstract base and its ContextAwareExceptionInterface contract, the typed subclasses, the getContext() shape, and the SignatureException named constructors are quoted verbatim from the source.
  • The “Actionable by:” triage lines are class-docblock contracts in those same files.
  • The human-factors anchor is Spec: ISO 9241-110 — §5.6.4.3, on errors that explain themselves enough to be fixed, and the §6 use error robustness principle. The engine treats the developer as the user and the exception as the interface that has to satisfy those clauses.

Catch broadly as a backstop, catch specifically where you can act, and feed the structured context straight to your logger — no message parsing.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

The specific catch recovers because the exception type told it that recovery was possible. The backstop logs structured context for everything else. At no point does the application read the message to find out what happened.

The usual misreading is that a deep exception tree is over-engineering, and that one error type would be simpler. It would be simpler for the engine and worse for you. One type means every failure is a generic stack trace and the recovery logic is a string match. That match is fragile; the next message reword breaks it. A small, specific hierarchy moves that knowledge into the type system, where the compiler and your catch blocks can use it.

A second misconception is that the message and the context are redundant. They are not. The message is prose for a human reading a log line. The context is a typed map for code routing, alerting, or dashboards. Conflating them is exactly the string-parsing trap the getContext() contract exists to remove.

The hierarchy is intentionally shallow. NextPDF does not create a distinct exception class for every conceivable failure. It creates one where catching that failure specifically is something a caller would reasonably do. Over-splitting would trade the string-parsing problem for a sprawling catch-list problem.

getContext() is structured for logs and APM, so it returns primitives and lists of primitives only, with no nested objects, by contract. It is diagnostic context, not a serialized snapshot of engine internals. It is also not a stable wire format to build external schemas against.

This page describes the exception design surface. The exact set of exceptions and their fields evolves with the engine. The classes and shapes quoted here are current as of this review and are illustrative of the contract, not a frozen catalogue. The contract — one base, typed subclasses, structured context, actionable messages — is the stable part.

  • Code-backed (evidence level) — a page whose claims are checked against the engine’s own source, quoted rather than paraphrased.
  • Context-aware exception — a NextPDF exception implementing ContextAwareExceptionInterface and exposing getContext(). That method returns a snake_case map of primitive diagnostic fields safe to serialize into a log or APM payload without parsing the message string.
  • Named constructor — a static factory method (for example SignatureException::tsaUrlEmpty()) that builds an exception with a message bound to a specific root cause and, often, its remedy.
  • PAdES — PDF Advanced Electronic Signatures, the ETSI profile family for PDF signing. Expanded on first use; covered in depth on the signing pages.
  • TSA — Time-Stamping Authority, the trusted service that issues RFC 3161 timestamps used by the higher PAdES profiles.