Exceptions

Alto uses typed exceptions for recoverable library failures. All Alto-owned domain exceptions implement MarkdownExceptionInterface, which provides one catch point at an application boundary.

Catch the right boundary

Catch a precise exception when the application has a recovery path. Catch MarkdownExceptionInterface only where the request, job, or command must turn any Alto failure into one application-level error.

<?php
use Alto\Markdown\Exception\MarkdownExceptionInterface;
use Alto\Markdown\Markdown;

try {
    $html = Markdown::github()->toHtml("# Guide\n");
} catch (MarkdownExceptionInterface $error) {
    throw new \RuntimeException('Markdown processing failed.', previous: $error);
}

Programmer errors inside application callbacks and extensions are not automatically converted into Alto exceptions. Do not catch Throwable around the whole application merely to hide those failures.

Recover by category

Catch these family types when several failures have the same application response:

Family Includes Recovery
ParseLimitException Source, block, inline, reference, and nesting limits Reject the input or retry only with an explicitly reviewed budget
FileException Read, write, and conflict failures Report $error->path, preserve pending edits, then branch on the subtype only when recovery differs
ResourceResolutionException Missing, denied, oversized, and unsupported resources Reject the reference or use an application fallback
InvalidExtensionException Duplicate extensions, invalid definitions, factories, parser results, lint results, formatter edits, and stats values Fail factory construction or the current operation and fix the trusted extension

InvalidExtensionException is also an InvalidMarkdownArgumentException. Existing configuration-level catch points therefore continue to work.

Use the precise subtype when its recovery is different:

Exception Typical cause Recovery
FileReadException The source path is not a readable regular file Check path authorization, type, existence, and permissions
FileWriteException A save failed, permissions could not be preserved, or the final target was rejected Keep the pending diff and report the filesystem failure
FileConflictException The target changed after open() Reopen, compare both versions, and reapply or abandon the edit
StaleHandleException A handle predates a reparse or save Query the current document again
MissingSectionException A mutation targeted a section that does not exist Check exists() or create it with ensure()
InvalidMarkdownArgumentException A rule ID, style, or public configuration value is invalid Fix application configuration
InvalidExtensionException A trusted extension violates its declared contract Fix or replace the extension, then rebuild the factory
InvalidMarkdownOperationException The requested edit has no safe V1 contract Choose a supported operation or edit a larger known-safe unit
PatchConflictException Pending edits overlap without a safe lowering Inspect the diff and split or reorder the operations
RenderException Rendering requires unavailable support or receives invalid state Correct the selected policy or runtime requirement
ResourceNotFoundException A requested external resource does not exist Report the missing reference or choose a fallback
ResourceDeniedException A path, symlink, type, read, or include tree violates resource policy Reject the reference without weakening the boundary
ResourceTooLargeException A resource exceeds its byte budget Reject it or use a separately reviewed resolver limit
UnsupportedResourceException The extension or origin ID is unsupported Select an allowed file type or the matching resolver

Parser limit subclasses expose the configured and attempted values. File exceptions expose the original application path through path. Resource limits and their exact counting rules are documented in Security.

Unknown generic node kinds also raise an Alto domain exception. Construct factories with custom extensions during application startup rather than after accepting a request.

Protect edits and files

Use exists() before mutating an optional section:

<?php
use Alto\Markdown\Markdown;

$document = Markdown::github()->fromString("# Guide\n");
$section = $document->section('Installation');

if (!$section->exists()) {
    $document->ensure()->section('Installation', 2)->apply();
}

Use compare-before-write when another process or editor may change the file:

<?php
use Alto\Markdown\Exception\FileConflictException;
use Alto\Markdown\Markdown;
use Alto\Markdown\Operation\SaveOptions;

$file = Markdown::github()->open('docs/installation.md');
$file->section('Run the first conversion')->append("\nUpdated.\n");

try {
    $file->save(new SaveOptions(compareBeforeWrite: true));
} catch (FileConflictException) {
    echo $file->diff()->toUnifiedString();
}

A failed protected save leaves the target and pending journal unchanged. diff() remains available for reconciliation.

After a successful save, requery nodes instead of reusing an earlier handle:

<?php
use Alto\Markdown\Markdown;

$file = Markdown::github()->open('docs/installation.md');
$file->format();
$file->save();

$currentTitle = $file->title()?->text();

The file API does not authorize paths for the host application. Validate the allowed directory before open() or saveAs(), and define the application's own symlink policy. Existing non-regular targets are rejected. A relative path is anchored to its canonical parent when opened or adopted by saveAs(), but that does not replace application authorization. Permission inspection or restoration failures abort atomic replacement and preserve the pending diff. See Editing for the complete persistence contract.