NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Alerts for Laravel

Timeouts & error handling

Per-check timeout

timeout(5) bounds a check at five seconds:

Health::for($team)->monitor(PaymentGatewayCheck::class)
    ->everyMinute()
    ->timeout(5)   // hard abort under CLI with ext-pcntl, post-hoc report elsewhere
    ->save();

On the CLI with ext-pcntl — queue workers included — the check is hard-aborted the moment the budget elapses, via SIGALRM. Elsewhere (web SAPI, Windows) it is a best-effort post-hoc report: the check runs to completion and is marked failed if it overran. A timed-out check flows through the normal failed-result path with a CheckTimedOut exception recorded in meta, plus timed_out_after.

The alarm aborts the check by throwing CheckTimedOut into it, so a broad catch (Throwable) inside the check — every built-in check has one — could swallow it. The timeout is still reported: once the alarm has fired, a check that returns normally or throws something else ends as a timed-out failed result, with timed_out_after in its meta. A check that keeps retrying is not aborted twice, so it can still run past its budget before that result is recorded.

Inside a queue worker the job’s own --timeout alarm is kept: a check budget longer than the time the job has left never extends it, and the worker’s alarm is re-armed after the check.

Exception-safe checks

A check’s check() never needs a try/catch. Any thrown exception is converted into a clean failed result and recorded as a normal alert and run, so a bad check never fails the queue job:

Health::define('flaky', function () {
    throw new RuntimeException('upstream exploded'); // becomes CheckResult::failed()
});

// Or build a result from a caught exception yourself:
CheckResult::fromException($e); // status failed; exception class, message and 15 stack frames in meta

Exception details stay in your logs

An exception’s message and stack frames can carry hosts, SQL and credentials, so by default they are neither stored nor mailed. The run and the alert get the message “The check threw RuntimeException; the details are in the application log.” — the alerts::checks.check_errored line, in English and Slovak — and keep only meta['exception'], the class. The throwable itself is reported to your exception handler, so it lands in your logs. The CheckResult a run-now returns still carries everything:

$result = Health::for($team)->run('flaky');

$result->meta['exception_message'];  // 'upstream exploded' — the caller of a run-now gets everything
$result->storedMessage();            // the message as stored and mailed (≤ CheckResult::MAX_STORED_MESSAGE_LENGTH, 1000)
$result->storedMeta();               // the meta as stored on runs and alerts

$alert = $team->alerts()->open()->first();

$alert->message;                     // 'The check threw RuntimeException; the details are in the application log.'
$alert->meta['exception'];           // 'RuntimeException' — the class is always stored
$alert->meta['timed_out_after'];     // seconds — present only for timeouts
  • Stored: a check’s own message, the package’s timeout text, meta['exception'] and timed_out_after. Left out: exception_message, exception_trace and the error a built-in check caught.
  • A timeout is not reported to the exception handler — its text is the package’s own and is stored. Built-in checks report the error they catch, whatever store_exception_details says.
  • storedMessage() and storedMeta() return exactly what is stored on runs and alerts — and mailed by the bundled notification.
  • A custom check that catches its own exception decides what it returns: whatever it puts in its message or meta is stored. Let it throw to get the treatment above.
  • To store and mail the details as before 1.2, set store_exception_details (ALERTS_STORE_EXCEPTION_DETAILS=true, a strict boolean): the exception’s message — up to 1000 characters in the message columns, in full in meta['exception_message'] — and its first 15 stack frames in meta['exception_trace'].

Only check() itself is guarded. Resolving the check (an unregistered key) or the owner (no HasNotifiablesForAlerts) still throws and fails the job — those are configuration errors you want to see.

Exceptions

All live in RoundlyConsulting\Alerts\Exceptions, apart from the toolkit one:

ExceptionThrown when
CheckTimedOutA check overruns its timeout() — also when the check catches it itself. Caught by the run and stored as a failed result — it never escapes. Carries public key and seconds.
InvalidCronExpressionmalformed(): a schedule is saved — monitor()->save(), schedule() or the trait shortcuts — with an expression that is not a valid 5-field cron or supported alias; nothing is stored. skipped(): runDue() met a stored row it cannot evaluate (written by a seeder or by hand) — reported to your exception handler, not thrown, and the rows after it are still dispatched.
InvalidHealthCheckdoesntExtendBaseCheck(): a class that does not extend Check is registered, run or scheduled. notRegistered(): a scheduled row names, or run() is given, a key that is not registered in the running process. ambiguous(): run() is given a class registered under several keys with as() — run it by key. invalidEscalationThreshold(): an escalation policy keyed by anything but a whole number of at least 1 — 0, a negative number, a name or a list-style ['owner', 'team']. invalidIdentity(): a uniqueBy() meta key holds a value that is not a scalar or null — the message names the key, never the value. notScheduledFor(): Health::for($owner)->run(), unmonitor() or reconfigure() is given a row that belongs to another owner.
InvalidRetentiondays(): Health::prune() is given fewer than 1 day — it would delete the whole run history. silenceDays(): Health::silences()->prune() is given fewer than 1 day. An InvalidArgumentException; nothing is deleted.
ScheduleConflictreconfigure() would move a uniqueBy() schedule onto the identity another live schedule of the check holds. A final RuntimeException naming the check, the row and the identity’s meta keys — never their values; nothing changes. Map it to 409 Conflict.
InvalidNotifiableForHealthCheckThe row’s owner does not implement HasNotifiablesForAlerts, or no longer exists.
InvalidConfigurationExceptionFrom package-toolkit: a config value is invalid — a model key that is not the packaged model or a subclass of it, an unknown key_type, a switch that is not a boolean, an unknown schedule.frequency, a non-string route.uri or route.name, or a history.retention_days or silence-retention that is not a whole number of at least 1. The message names the key.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.