Go does not have exceptions. It has errors. Errors are returned rather than thrown, and they do not inherently capture a stack trace.
Go errors can also wrap other errors. This is broadly similar to a PHP exception accepting a previous exception:
throw new RuntimeException('Operation failed', previous: $e);There is one critical difference. In Go, code can test for an error type anywhere in the wrapped error chain. It is not limited to inspecting the outermost error:
var matched *DatabaseError
if errors.As(err, &matched) {
// matched is the closest DatabaseError in the chain.
}This makes wrapping errors far less disruptive. A function can add useful context without breaking callers that handle the original error type.
PHP does not provide an equivalent language construct. A catch clause considers only the exception that was directly thrown. Finding a particular exception type within the chain requires manually traversing Throwable::getPrevious():
try {
performOperation();
} catch (Throwable $thrown) {
for ($matched = $thrown; $matched !== null; $matched = $matched->getPrevious()) {
if ($matched instanceof UserException) {
// Handle the UserException.
break;
}
}
throw $thrown;
}To that end, I propose adding a catchAny clause:
try {
performOperation();
} catchAny (UserException $matched) {
// $matched is the closest UserException in the exception chain.
}catchAny would traverse the exception chain beginning with the exception originally thrown and continuing through each value returned by Throwable::getPrevious().
The clause would match the first exception compatible with the declared type. Therefore, $matched would contain the compatible exception closest to the exception originally thrown.
The originally thrown exception could optionally be captured using a second parameter:
try {
performOperation();
} catchAny (UserException $matched, $thrown) {
// $matched is the closest UserException in the exception chain.
// $thrown is the exception originally thrown.
}This would allow libraries to wrap exceptions with additional context without hiding their underlying meaning from callers.
For example:
try {
loadUser($id);
} catch (DatabaseException $e) {
throw new UserRepositoryException(
'Unable to load user',
previous: $e,
);
}A caller could still handle the underlying database error directly:
try {
$repository->loadUser($id);
} catchAny (DatabaseException $matched) {
// Handle the underlying database failure.
}The behavior of existing catch clauses would remain unchanged. catchAny would be an explicit request to search the complete exception chain.
catch and catchAny clauses would be evaluated together in source order. The first matching clause would handle the exception:
try {
$repository->loadUser($id);
} catch (UserRepositoryException $e) {
// Handles the outer exception first.
} catchAny (DatabaseException $matched) {
// Reached only if the earlier catch clause did not match.
}Reversing the clauses would allow the underlying database failure to take precedence:
try {
$repository->loadUser($id);
} catchAny (DatabaseException $matched) {
// Handles a DatabaseException anywhere in the chain.
} catch (UserRepositoryException $e) {
// Reached only if the earlier catchAny clause did not match.
}When the optional $thrown parameter is present without an explicit type, it would be known only to implement Throwable. It could instead declare a more specific type:
try {
$repository->loadUser($id);
} catchAny (
DatabaseException $matched,
UserRepositoryException $thrown,
) {
// Handle a database failure wrapped by a repository exception.
}When the second parameter declares a type, the clause would match only when both conditions are satisfied:
- The originally thrown exception is compatible with the second parameter's declared type.
- The exception chain contains an exception compatible with the first parameter's declared type.
In this example, the originally thrown exception must be a UserRepositoryException, and its exception chain must contain a DatabaseException.
This would allow callers to distinguish between identical underlying failures encountered through different abstraction layers.