Semantic Exceptions

“I have not failed. I’ve just found 10,000 ways that won’t work”

  —Thomas Edison (1847-1931)

Meaningful Failures

Generic exceptions tell what happened:

catch (Exception $e) {
    echo $e->getMessage();  // "Validation failed"
}

In contrast, semantic exceptions tell why existence is impossible:

catch (SemanticVariableException $e) {
    foreach ($e->getErrors()->exceptions as $exception) {
        echo get_class($exception) . ": " . $exception->getMessage();
        // EmptyNameException: Name cannot be empty
        // InvalidEmailException: Invalid email format
    }
}

Domain Exception Classes

All exceptions inherit from DomainException. Technical exceptions (RuntimeException, InvalidArgumentException, etc.) are not used. Failures are always expressed as failures with domain meaning:

abstract class DomainException extends Exception {}

final readonly class EmptyNameException extends DomainException {}

final readonly class InvalidEmailException extends DomainException
{
    public function __construct(public string $invalidEmail)
    {
        parent::__construct("Invalid email format: {$invalidEmail}");
    }
}

// Age-related existence failures
abstract class AgeException extends DomainException {}
final readonly class NegativeAgeException extends AgeException {}
final readonly class AgeTooHighException extends AgeException {}

Domain exceptions hold not just messages but structured data. From the $invalidEmail property, programs can access the invalid email address value—for display, API responses, logging, and more:

catch (InvalidEmailException $e) {
    $logData = [
        'invalid_email' => $e->invalidEmail,    // Programmatically accessible
        'user_ip' => $request->getClientIp(),
        'timestamp' => now()
    ];
    Logger::warning('Invalid email attempt', $logData);
}

Multilingual Messages

The #[Message] attribute lets exceptions speak in the user’s language:

#[Message([
    'en' => 'Name cannot be empty.',
    'ja' => '名前は空にできません。',
    'es' => 'El nombre no puede estar vacío.'
])]
final readonly class EmptyNameException extends DomainException {}

#[Message([
    'en' => 'Age must be at least {min} years.',
    'ja' => '年齢は最低{min}歳でなければなりません。'
])]
final readonly class AgeTooYoungException extends DomainException
{
    public function __construct(public int $min = 13) {}
}

Error Collection

The framework does not stop at the first error—it collects all validation errors:

try {
    $user = $becoming(new UserInput('', 'invalid-email', 10));
} catch (SemanticVariableException $e) {
    // Three errors collected simultaneously:
    // - EmptyNameException
    // - InvalidEmailException
    // - AgeTooYoungException

    $messages = $e->getErrors()->getMessages('en');
    // ["Name cannot be empty", "Invalid email format", "Age must be at least 13"]
}

Rather than “fail fast”—understand all problems at once.

Errors as Existence

Error states can be treated as valid metamorphosis results:

#[Be([ValidUser::class, InvalidUser::class])]
final readonly class UserValidation
{
    public ValidUser|InvalidUser $being;

    public function __construct(#[Input] string $data)
    {
        try {
            $this->being = new ValidUser($data);
        } catch (ValidationException $e) {
            $this->being = new InvalidUser($e->getErrors());
        }
    }
}

Rather than halting execution with exceptions, errors are expressed as types. Failure, like success, is a legitimate result of transformation.


For the full picture, see Reference ➡️