/ Content
Saw a post on X last week that mentioned a handful of Laravel global helpers I'd walked past a hundred times. Not because they're hidden. They're right there in Illuminate/Foundation/helpers.php, sitting alongside config() and route(). I just never stopped to actually read them.
The functions: throw_if, throw_unless, report, report_if, report_unless, and rescue. Each one is a piece of syntax sugar for exception handling boilerplate you're probably writing the long way right now.
throw_if and throw_unless
You've written this pattern constantly:
if (! $user->isAdmin()) {
throw new AuthorizationException('User is not an admin.');
}
throw_unless collapses it to one line:
throw_unless($user->isAdmin(), AuthorizationException::class, 'User is not an admin.');
throw_if flips the condition:
throw_if($user->isBanned(), UserBannedException::class, 'Your account has been suspended.');
Here's what they actually look like under the hood:
function throw_if(mixed $condition, Throwable|string $exception = 'RuntimeException', mixed ...$parameters): mixed
{
if ($condition) {
throw (is_string($exception) ? new $exception(...$parameters) : $exception);
}
return $condition;
}
function throw_unless(mixed $condition, Throwable|string $exception = 'RuntimeException', mixed ...$parameters): mixed
{
throw_if(! $condition, $exception, ...$parameters);
return $condition;
}
throw_unless is literally just throw_if with the condition inverted. You can pass a class name as a string or a pre-built exception instance. The variadic $parameters get forwarded to the exception constructor when you pass a class name, so constructor arguments work naturally.
Both functions return $condition. That means you can write something like throw_unless($model->save(), RuntimeException::class, 'Save failed') and it returns true on the happy path, like the examples above. Useful for dead-simple cases, but stacking these inside more complex expressions can quickly become harder to follow than the if block you were trying to replace.
One practical note: these helpers are best for conditions that represent programmer error or or authorization failures. Things that should never be true, or things that indicate something is fundamentally wrong with the request. For more complex conditional logic, a full if block is still the right call. These shine when it's a simple boolean check.
report()
Throwing stops execution. Sometimes that's not what you want. Sometimes something goes wrong and the correct response is to log it, send it to Bugsnag or Sentry, and keep the request running.
That's report().
function report(Throwable|string $exception, mixed ...$contexts): void
{
if (is_string($exception)) {
$exception = new RuntimeException($exception);
}
app(ExceptionHandler::class)->report($exception);
}
It hands the exception directly to Laravel's exception handler, which routes it to whatever you've configured. If you haven't set up an external error tracker, it falls through to your log file by default. The request keeps going either way.
Before I knew about this, I'd write manual catch blocks that looked like this:
try {
$this->webhookClient->dispatch($payload);
} catch (Throwable $e) {
Log::error('Webhook dispatch failed', ['exception' => $e->getMessage()]);
}
With report() it's just:
try {
$this->webhookClient->dispatch($payload);
} catch (Throwable $e) {
report($e);
}
Laravel handles the routing. You get the full stack trace in your error tracker instead of just the message string you remembered to include.
Two other things worth noting. You can pass a plain string instead of a Throwable. Laravel wraps it in a RuntimeException for you. So this is valid:
report("Payment processor returned an unexpected status: {$status}");
No need to manually instantiate an exception when you just want to track a message
Second, calling report() doesn't suppress the exception if you're in a catch block. It just reports it. If you want execution to continue, you're responsible for catching. report() is just a delivery mechanism.
report_if and report_unless
These are the conditional versions of report(), and their internals are about as simple as you'd expect:
function report_if(bool $boolean, Throwable|string $exception, mixed ...$contexts): void
{
if ($boolean) {
report($exception, ...$contexts);
}
}
function report_unless(bool $boolean, Throwable|string $exception, mixed ...$contexts): void
{
if (! $boolean) {
report($exception, ...$contexts);
}
}
You'd reach for these when a condition is worth tracking but not worth crashing over. Data inconsistencies, edge cases that shouldn't happen but might, third-party integrations behaving oddly:
report_if(
$order->total !== $expectedTotal,
"Order {$order->id} total mismatch: expected {$expectedTotal}, got {$order->total}"
);
That logs the mismatch to your error tracker if the totals don't match, but the request keeps running. You'll see it in Bugsnag, investigate, and fix the root cause without the order flow ever blowing up on the user.
rescue()
rescue() is the most interesting one. It wraps a callable in a try/catch block, handles reporting automatically, and returns a fallback value if anything goes wrong.
function rescue(callable $callback, $rescue = null, bool $report = true): mixed
{
try {
return $callback();
} catch (Throwable $e) {
if ($report) {
report($e);
}
return value($rescue, $e);
}
}
The third parameter, $report, controls whether caught exceptions get forwarded to your error tracker. It defaults to true, so unless you explicitly pass false, every exception rescue() catches is also reported. That's the connection between rescue() and report() built directly into the function.
The fallback value (the second parameter) can be a scalar, null, or a closure. When it's a closure, it receives the caught exception as its argument:
$profile = rescue(
fn () => $this->externalApi->getUserProfile($userId),
fn (Throwable $e) => $this->getCachedProfile($userId),
);
Compare that to the try/catch equivalent:
try {
$profile = $this->externalApi->getUserProfile($userId);
} catch (Throwable $e) {
report($e);
$profile = $this->getCachedProfile($userId);
}
Same behavior. The rescue() version makes the intent more compact: try this, fall back to that, report failures. The try/catch version spreads that same intent across four lines.
If you want to suppress reporting for expected failures, pass false as the third argument:
$result = rescue(
fn () => $this->cache->get($key),
null,
false
);
No noise in your error tracker for something you're already handling gracefully.
One thing to be careful about: rescue() catches all Throwable, not specific exception types. That's the right call for external API calls and third-party integrations where failure modes are broad. It's the wrong call for business logic where specific exception types carry meaning. If you rescue() around a method that throws a DomainException for a specific rule violation, you'll swallow that exception silently and return null. That's a bad time.
The difference between throw and report
The reason these helpers fall into two groups comes down to one question: can the application continue?
throw_if and throw_unless are for conditions where the answer is no. Authorization failures, missing required data, invalid state that makes further processing meaningless. You throw because continuing would produce wrong results or create a security problem.
report, report_if, report_unless, and rescue are for conditions where the answer is yes. External API failures, non-critical operations going wrong, data anomalies worth tracking. You report because stopping the request would be worse than logging the problem and moving forward.
Confusing the two is where subtle bugs come from. Using rescue() around a permission check means silently swallowing authorization failures. Using throw_if when a payment processor returns an unexpected status code means your users see a 500 page instead of a graceful fallback.
These helpers are worth knowing, but it's easy to overdo it. Replacing every if block and try/catch with one-liners can make code harder to scan, not easier, especially for developers who haven't seen these functions before. The goal is clearer intent, not fewer lines. If a rescue() call with a closure fallback and $report = false is harder to read than the four-line try/catch it replaced, use the try/catch.