Exception Handling
This guide covers the exceptions FulcrumFS raises, when they occur, and the event-based model used for commit and rollback failures (which are not thrown).
Two failure styles
Most errors surface as exceptions you catch at the call site, which is the natural shape for input validation: a bad upload becomes an exception in the upload handler that you translate into an HTTP 400. Commit and rollback failures are different: because the affected files stay readable and the application has no useful way to "undo" a half-finished commit, they are reported through events and the files are marked indeterminate for later resolution. Understanding which style applies where is the key to handling failures correctly.
Fetch Errors
Fetching a file or variant that does not exist throws RepoFileNotFoundException. This applies to GetAsync, OpenAsync, and GetVariantAsync.
// "GET /api/documents/{id}" handler.
try
{
var info = await repo.GetAsync(doc.FileId);
return Results.File(info.Open(), contentType: doc.ContentType);
}
catch (RepoFileNotFoundException)
{
// The repository has no file for this row. Most likely the database
// and repository have drifted apart, which warrants investigation.
logger.LogError("Document {Id} references missing file {FileId}.", doc.Id, doc.FileId);
return Results.NotFound();
}
Processing Errors
A failure during add-time processing throws FileProcessingException. This covers content that fails format validation (a .jpg upload that is really an EXE), a FileProcessingPipelineSelector with no matching pipeline and no default (a file extension the endpoint does not accept), and errors raised by a processor (a corrupt image that ImageSharp cannot decode). Catch it around AddAsync to reject bad uploads.
// "POST /api/photos" handler.
try
{
var added = await txn.AddAsync(source, ".jpg", leaveOpen: true, pipeline);
// ... commit database, commit repository ...
return Results.Ok();
}
catch (FileProcessingException ex)
{
// The upload was rejected. The transaction will be rolled back on dispose.
return Results.BadRequest(new { error = ex.Message });
}
Tip
Place format validation as the first step in a pipeline so invalid content is rejected before any expensive transformation runs. See Validating File Formats.
Unchanged main sources
When a main-file pipeline sets ThrowWhenMainSourceUnchanged to true, adding content the pipeline leaves unchanged throws FileSourceUnchangedException. This is useful for detecting an accidental re-add of identical content; for example, a re-import that is supposed to produce a transformed copy can flag the case where the input was already in the target format. See Processing Pipelines.
Variant Collision Errors
Adding a variant that already exists with AddVariantAsync throws InvalidOperationException. Choose the right method for your intent:
- AddVariantAsync when the variant is required to be new (caller error otherwise).
- TryAddVariantAsync when you want to attempt the add and get
nullback on collision. - GetOrAddVariantAsync when you want "ensure this variant exists" semantics, for example a lazy thumbnail generator that runs on first request.
See File Variants.
Commit and Rollback Failures
Commit and rollback do not throw on failure. Instead, the affected files are marked indeterminate and remain readable, and the failure is reported through a single event:
- TransactionCompletionFailed fires when either a CommitAsync or a rollback (explicit or via disposal) cannot fully complete. The handler receives a RepoTransactionCompletionFailureInfo exposing the failed Operation (a RepoTransactionCompletionOperation value of
CommitorRollback) and the underlying Exception (an AggregateException if multiple errors occurred during the step).
Subscribe to log or alert. If you want throwing behavior at the call site, the handler can raise an exception itself - handler exceptions are not caught and will propagate out of the completion call.
repo.TransactionCompletionFailed += failure =>
{
if (failure.Operation is RepoTransactionCompletionOperation.Commit)
logger.LogError(failure.Exception, "Repository commit failed; affected files are indeterminate and will be resolved during cleanup.");
else
logger.LogWarning(failure.Exception, "Repository rollback failed; affected files are indeterminate.");
return Task.CompletedTask;
};
Important
Indeterminate files left by these failures are resolved later during cleanup, where a callback decides per FileId whether to Keep or Delete each one. This is what closes the loop on commit failures, so make sure cleanup is scheduled in any environment that runs the repository. See Repository Cleanup and Transactional Commit Model.
Corruption Detection
FulcrumFS surfaces detected repository corruption through the CorruptionDetected event. The handler is a RepoCorruptionHandler that receives a RepoCorruptionInfo describing the issue: a RepoCorruptionKind, the affected FileId, an optional variant ID, and a descriptive message. Every code path that throws because of repository corruption fires this event first, so consumers can observe and repair corruption regardless of which entry point detected it.
The currently detected kinds are:
- DanglingAlias - an alias marker whose source data file is missing. Cannot occur during normal API usage; indicates external interference (a partial backup restore, manual file deletion, a crash recovery failure). Surfaced at the call site as DanglingAliasException (a subtype of RepoFileNotFoundException).
- MalformedAlias - an alias marker whose filename cannot be parsed or otherwise violates a structural invariant. The repository treats malformed alias markers as nonexistent.
- DuplicateVariantEntry - more than one file in a file group directory maps to the same logical variant slot (for example, two data files for the main file, or two alias markers for the same variant ID). The slot cannot be safely resolved without manual intervention; throws RepoCorruptedException.
- RebaseInconsistency - a rebase operation cannot be resumed because required files are missing (the chosen survivor has neither a materialized data file nor an alias marker, or the source data file is missing while the chosen variant is still an unmaterialized alias). Throws RepoCorruptedException.
- OrphanRebaseMarker - a rebase marker observed with no source data, chosen data, or surviving alias dependents - residue from a rebase whose final marker-delete step was lost. Surfaced for visibility only; no exception is thrown and the marker is physically removed inline.
The handler is asynchronous (Task-returning) so consumers can perform I/O-bound logging or alerting work without resorting to sync-over-async. Handlers are awaited sequentially before the detecting operation returns, so keep them fast; any handler exception is not caught and will propagate out of the operation that fired the event.
repo.CorruptionDetected += info =>
{
logger.LogError(
"Repository corruption detected: kind={Kind}, fileId={FileId}, variantId={VariantId}, message={Message}",
info.Kind, info.FileId, info.VariantId, info.Message);
return Task.CompletedTask;
};
In addition to the event, dangling aliases are surfaced at the call site:
- GetVariantAsync and OpenVariantAsync throw DanglingAliasException (a subtype of RepoFileNotFoundException).
- GetGroupAsync omits the dangling alias from VariantFiles and exposes it via DanglingAliases.
- Adding a variant over a dangling alias self-heals: the dangling marker is removed and the add proceeds normally.
See Variant Aliasing for the full dangling-alias model.
Next Steps
- Transactional Commit Model - The indeterminate state in depth.
- Repository Cleanup - Resolving indeterminate files.
- Adding and Committing Files - Where commit failures originate.