Skip to content

Guard Clause (Fail-Fast)

The Guard Clause pattern validates preconditions at the start of a method and immediately throws a semantic exception if a condition is not met. This pattern prevents invalid states from propagating and produces clear, actionable error messages.

flowchart TD
    E[Method entry] --> G1{blobId valid?}
    G1 -->|no| X1["throw BlobNotFoundException"]
    G1 -->|yes| G2{Status = Valid?}
    G2 -->|no| X2["throw BlobNotValidException"]
    G2 -->|yes| G3{tenant available?}
    G3 -->|no| X3["throw ForbiddenException"]
    G3 -->|yes| BL[Business logic]

    X1 --> PD["GranitExceptionHandler<br/>ProblemDetails RFC 7807"]
    X2 --> PD
    X3 --> PD
ExceptionFileHTTP CodeErrorCode
BusinessExceptionsrc/Granit.Core/Exceptions/BusinessException.cs400Configurable
NotFoundExceptionsrc/Granit.Core/Exceptions/NotFoundException.cs404
EntityNotFoundExceptionsrc/Granit.Core/Exceptions/EntityNotFoundException.cs404
ConflictExceptionsrc/Granit.Core/Exceptions/ConflictException.cs409Configurable
ForbiddenExceptionsrc/Granit.Core/Exceptions/ForbiddenException.cs403
ValidationExceptionsrc/Granit.Core/Exceptions/ValidationException.cs422Field errors
BlobNotFoundExceptionsrc/Granit.BlobStorage/Exceptions/BlobNotFoundException.cs404BlobStorage:NotFound
FeatureLimitExceededExceptionsrc/Granit.Features/Exceptions/FeatureLimitExceededException.cs429Features:LimitExceeded
FeatureNotEnabledExceptionsrc/Granit.Features/Exceptions/FeatureNotEnabledException.cs403Features:NotEnabled

All exceptions are intercepted by GranitExceptionHandler (src/Granit.ExceptionHandling/GranitExceptionHandler.cs) and converted to RFC 7807 ProblemDetails.

Exceptions that do not implement IUserFriendlyException have their message masked in production (“An unexpected error occurred”) to prevent leaking internal schema details.

Guard clauses make preconditions explicit and document each method’s contract. Semantic exceptions allow the global middleware to produce appropriate HTTP responses without each endpoint managing its own errors.

public static class DownloadDocumentHandler
{
public static async Task<PresignedDownloadUrl> Handle(
DownloadDocumentQuery query,
IBlobStorage blobStorage,
CancellationToken cancellationToken)
{
// Guard clause -- throws BlobNotFoundException (404)
BlobDescriptor? descriptor = await blobStorage.GetDescriptorAsync(
"medical-documents", query.BlobId, ct)
?? throw new BlobNotFoundException(query.BlobId);
// Guard clause -- throws BlobNotValidException (409)
if (descriptor.Status != BlobStatus.Valid)
throw new BlobNotValidException(query.BlobId, descriptor.Status);
// Business logic -- preconditions are guaranteed
return await blobStorage.CreateDownloadUrlAsync(
"medical-documents", query.BlobId, cancellationToken: ct);
}
}