Implementing Cron Job Error Handling and Logging Correctly
Implementing Cron Job Error Handling and Logging Correctly
~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 30 showed why an observer on sales_order_place_after must never let an exception escape - it would break the customer's checkout request. ExpirePoints (chapter 33) follows a structurally different, but similarly important rule: no customer synchronously waits on a cron job, yet an unhandled error here can still cause real damage - just of a different kind.
What happens on an uncaught exception?
Unlike a checkout observer, an exception in a cron job method doesn't abort the whole cron:run invocation - Magento's own execution mechanism (\Magento\Cron\Observer\ProcessCronQueueObserver) catches it per job, marks the affected row in the cron_schedule table with status = 'error', writes the error message into the messages column, and moves on to the next due job. Other groups and jobs keep running unaffected.
Achtung: That only applies at the outermost level - the execute() method Magento itself calls. The try/catch inside the foreach loop from chapter 33 is still necessary: without it, an exception on the second of 500 due customers would abort the entire rest of the loop - the remaining 498 customers would only be processed on the next cron run (per chapter 32, at the earliest the next day). Magento's own outer catch only protects the overall process, not the individual iterations inside a job.
Where errors end up
cron_schedule.status/cron_schedule.messages- the short-term, technical view of a failed run.var/log/system.log/var/log/exception.log- Magento's default log files, unless a dedicated logger channel is configured (see below).var/log/cron.log- pure execution logs ofcron:runitself (start/end/duration per job), no substitute for structured error logging.
# Tail directly inside the container (the bin/log wrapper from CLAUDE.md)
bin/log cron.log
bin/log system.logA dedicated log channel for this module
Without configuration, every LoggerInterface call ends up in var/log/system.log, mixed in with every other module. A dedicated Monolog channel - pure di.xml configuration, no new class needed - writes loyalty-specific log lines to their own file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<virtualType name="Mironsoft\Loyalty\Logger\Handler"
type="Magento\Framework\Logger\Handler\Base">
<arguments>
<argument name="fileName" xsi:type="string">var/log/mironsoft_loyalty.log</argument>
</arguments>
</virtualType>
<virtualType name="Mironsoft\Loyalty\Logger\Logger" type="Monolog\Logger">
<arguments>
<argument name="name" xsi:type="string">mironsoftLoyalty</argument>
<argument name="handlers" xsi:type="array">
<item name="system" xsi:type="object">Mironsoft\Loyalty\Logger\Handler</item>
</argument>
</arguments>
</virtualType>
<type name="Mironsoft\Loyalty\Cron\ExpirePoints">
<arguments>
<argument name="logger" xsi:type="object">Mironsoft\Loyalty\Logger\Logger</argument>
</arguments>
</type>
</config>The <type name="...ExpirePoints"> block only overrides, for this one class, which concrete LoggerInterface implementation gets injected into the $logger constructor parameter - AwardPointsOnOrderPlaced (chapter 30) and ReversePointsOnCreditmemoSave (chapter 31) could be switched to Mironsoft\Loyalty\Logger\Logger the same way, but are deliberately left on the default channel here, so critical, checkout-adjacent error output doesn't get hidden in a file nobody monitors routinely.
The "missed" status
Alongside error, cron_schedule also knows missed: a job gets marked as missed and not executed if its scheduled time is more than schedule_lifetime minutes (chapter 32, 180 here) overdue - for example after an extended server outage, or when a previous run of the same group was blocking. For a daily job with a three-hour schedule_lifetime, that means: if the server is down for more than three hours at the scheduled run time, point expiry simply doesn't run that day - but it automatically catches up the next day, because the reconciliation from chapter 33 makes no distinction between "became due yesterday" and "became due today".
Tipp: For local testing, a manual call through the bin/ wrapper is enough - no need to wait for the next scheduled run.
bin/magento cron:run --group=mironsoft_loyalty
bin/log mironsoft_loyalty.logWith robust error handling on both sides - observers (chapters 30/31) and the cron job (chapters 33/34) - chapter 35 turns to the other direction: this module itself as the publisher of its own events for other modules.