Exception handling best practices

Last update: 2023-10-30
  • Created for:
  • Experienced
    Developer

If an exception is not written to the exception.log file with the exception model as context, it is not recognized and analyzed correctly in New Relic or other PSR-3 monolog-compatible log storage. Logging only a part of the exception (or logging it to the wrong file) leads to bugs in production when exceptions are overlooked.

Correct exception handling

The following checklist provides examples to demonstrate correct exception handling.

correct Write to the exception log

Write to the exception log using the following pattern, regardless of further actions, unless there is a compelling reason not to.

try {
    $this->productRepository->getById($sku);
} catch (Exception $e) {
    $this->logger->critical($e);
}

This approach automatically saves the $e->getMessage to the log message and the $e object to the context, following the PSR-3 context standard. This is done in \Magento\Framework\Logger\Monolog::addRecord.

correct Mute signals

Mute signals by not logging exceptions that are part of the intended operations flow. No follow-up action is necessary when the exception is encountered, so it does not need to be logged and analyzed when it occurs. Add a comment indicating the reason for muting signals and that it is intentional. Combine with phpcs:ignore.

try {
    $this->productRepository->deleteById($sku);
} catch (NoSuchEntityException $e) { // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
    // Product already removed
}

correct Downgrade exceptions

Downgrade exceptions by following the PSR-3 context standard.

try {
    $this->productRepository->getById($sku);
} catch (Exception $e) {
    $this->logger->debug($e->getMessage(), ['exception' => $e]);
}

correct Logging always comes first

As a best practice logging always comes first in the code to prevent cases where another exception or fatal error is thrown before writing to the log.

try {
    $this->productRepository->getById($sku);
} catch (Exception $e) {
    $this->logger->critical($e);
    $this->alternativeProcedure();
}

correct Log messages and the entire exception trace

Log messages and the entire exception trace by following the PSR-3 context standard.

try {
    $this->productRepository->getById($sku);
} catch (Exception $e) {
    $this->logger->critical($e->getMessage(), ['exception' => $e, 'trace' => $e->getTrace()]);
}

Incorrect exception handling

The following examples demonstrate incorrect exception handling.

incorrect Logic before logging

Logic before logging can lead to another exception or fatal error, which prevents the exception from being logged and should be replaced by correct example.

try {
    $this->productRepository->deleteById($sku);
} catch (NoSuchEntityException $e) {
    $this->alternativeProcedure();
    $this->logger->critical($e);
}

incorrect Empty catch

Empty catch blocks can be a sign of unintended muting and should be replaced by the correct example.

try {
    $this->productRepository->deleteById($sku);
} catch (NoSuchEntityException $e) {
}

incorrect Double localization

If the caught localized exception is not translated yet, resolve the problem at the place where the exception is thrown the first time.

try {
    $this->productRepository->getById($sku);
} catch (LocalizedException $e) {
    throw new LocalizedException(__($e->getMessage()));
}

incorrect Log messages and trace to different log files

The following code incorrectly logs the stack trace for an exception as a string to a log file.

try {
    $this->productRepository->getById($sku);
} catch (\Exception $e) {
    $this->logger->error($e->getMessage());
    $this->logger->debug($e->getTraceAsString());
}

This approach introduces line breaks in the message, which is not compliant with PSR-3. The exception, including stack trace, must be part of the message context to ensure that it is saved correctly with the message in New Relic or other PSR-3 monolog-compatible log storage.

Fix this problem by replacing the code following the correct examples shown in Write to the exception log or Downgrade exceptions.

incorrect Downgrade exceptions without context

The exception is downgraded to an error, which does not allow an object to be passed, but only a string, hence the getMessage(). This causes the trace to be lost and should be replaced by the correct examples shown in Write to the exception log or Downgrade exceptions.

try {
    $this->productRepository->getById($sku);
} catch (\Exception $e) {
    $this->logger->error($e->getMessage());
}

incorrect Log only the message to the exception log

Instead of passing the object $e, only $e->getMessage() is passed. This causes the trace to be lost and should be replaced by the correct examples shown Write to the exception log or Downgrade exceptions.

try {
    $this->productRepository->getById($sku);
} catch (\Exception $e) {
    $this->logger->critical($e->getMessage());
}

incorrect Missing // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch

Omitting the phpcs:ignore line triggers a warning in PHPCS and should not pass your CI. This should be replaced by the correct example shown in Mute signals.

try {
    $this->productRepository->deleteById($sku);
} catch (NoSuchEntityException $e) {
    // Product already removed
}

On this page