Handling errors
When something fails, Stewart throws an exception. You can let it go, or catch it where your app can do something better. This app counts doorbell rings, and starts again when the stored count is not a number:
<?php
declare(strict_types=1);
namespace App;
use Psr\Log\LoggerInterface;use Stewart\Contracts\App;use Stewart\Contracts\Automation;use Stewart\Contracts\Exception\StoreError;use Stewart\Contracts\Exception\StoreException;use Stewart\Contracts\HaContext;use Stewart\Contracts\Store\Store;
#[Automation(id: 'visit-counter')]final readonly class VisitCounter implements App{ public function __construct( private HaContext $ha, private Store $store, private LoggerInterface $logger, private string $doorbell = 'binary_sensor.doorbell', ) {}
public function initialize(): void { $this->ha->watchStateChanges($this->doorbell)->whenChangedTo('on')->subscribe(function (): void { try { $this->store->increment('visits'); } catch (StoreException $e) { if ($e->reason !== StoreError::NotIncrementable) { throw $e; }
$this->logger->warning('The visit count was not a number; starting again', ['exception' => $e]); $this->store->set('visits', 1); } }); }
public function dispose(): void {}}When you do not catch it
Section titled “When you do not catch it”- In a handler, Stewart logs
App failedwith the exception, counts the failure inmake status, and the app keeps running. The next event runs the handler again. - In
initialize(), the app does not start. Other apps keep running.
So catch an exception only when the app can do something useful, such as trying another way, sending a notification, or skipping one item.
Which exception, and why
Section titled “Which exception, and why”Every exception Stewart throws extends Stewart\Contracts\Exception\StewartException. Each part of Stewart has its
own class:
| Class | Thrown by |
|---|---|
ServiceCallException |
callService(), callServiceForResponse() and the generated service methods |
HistoryException |
getHistory() |
EventFireException |
fireEvent() |
StateException |
requireState() for an entity that does not exist, and invalid operator settings |
StoreException |
Store and ReadableStore |
ScheduleException, TimeException |
The Scheduler, and invalid times or durations |
WaitException |
waitForState() and waitForNext(), when the stream ends or the app stops first |
ExposureException, CommandException |
Exposed entities |
RegistryEditException |
RegistryEditor |
IdentifierException |
An invalid entity ID, or one the typed classes do not know |
TopicException, MqttException, TriggerException, SunException |
Messages between apps, MQTT, triggers, the sun |
$e->reason says what exactly failed. It is an enum named after the class: StoreException has StoreError,
ServiceCallException has ServiceCallError, and so on. Compare the reason, as the example does, not the message:
messages are written for people and may change, reasons do not. The error reference lists
every reason.
$e->context holds what the error is about as an array, for example ['domain' => 'light', 'service' => 'turn_on', 'detail' => '…'].
In the logs
Section titled “In the logs”Log the exception under the exception key, as the example does. Stewart then adds its class, reason and context to
the log line. With log_format: json, that is the extra.error field:
{ "message": "App failed", "context": {"app": "hall-light", "exception": "…"}, "extra": { "error": { "class": "Stewart\\Contracts\\Exception\\ServiceCallException", "reason": "rejected", "context": {"domain": "light", "service": "turn_on", "detail": "Entity not found"} } }}make status shows each app’s last failure with its reason.
Testing
Section titled “Testing”A test sees an exception from a handler directly: the harness throws it out of the pushState(), pushEvent() or
advanceTime() that ran the handler. To test a failing service, use stubServiceFailure(); see
Calling services.
<?php
declare(strict_types=1);
namespace App\Tests;
use App\VisitCounter;use PHPUnit\Framework\TestCase;use Stewart\Contracts\App\AppId;use Stewart\Testing\App\AppHarness;
final class VisitCounterTest extends TestCase{ public function testACorruptCountStartsAgain(): void { $harness = new AppHarness(); $store = $harness->stores->openAppStore(new AppId('visit-counter')); $store->set('visits', 'many'); $harness->ha->seedState('binary_sensor.doorbell', 'off'); $harness->startApp(VisitCounter::class);
$harness->ha->pushState('binary_sensor.doorbell', 'on');
self::assertSame(1, $store->getInt('visits')); self::assertCount(1, $harness->logger->listMessagesAt('warning')); }}