Keep the original exception when adding domain context
A supplier client throws a low-level exception. The application wants to report “catalog import failed,” but replacing the exception with a new message loses the original stack and cause. Add context while preserving the previous exception.
Wrap the cause deliberately
<?php
try {
try {
throw new RuntimeException('Example transport failure');
} catch (RuntimeException $cause) {
throw new RuntimeException('Catalog import failed', 0, $cause);
}
} catch (RuntimeException $error) {
assert($error->getPrevious()?->getMessage() === 'Example transport failure');
}
The outer exception describes the operation that failed. The previous exception retains details useful to diagnostics. Do not copy credentials, request bodies, or complete provider responses into the outer message simply because it is easier to search.
Catch at a useful boundary
Catching every exception in every helper adds noise. Wrap an exception where the operation gains meaningful business context, such as an import attempt identifier. At the request boundary, present a safe error to the user and log the detailed exception with access controls.
Some failures should be retried; others should be corrected. A malformed supplier payload is not automatically fixed by running the same operation ten times. Preserve error categories so a job handler can make that distinction.
Test the public response separately from the diagnostic chain. The response should avoid internals, while the captured exception should still expose its cause to the test. Logging only the final string message can undo much of the benefit of chaining, so verify your logging path accepts the exception object.
Reference
Official documentation. The examples here illustrate the stated boundary; adapt them to your application and test its failure paths.