skip to content

In Laravel, what do abort(), abort_if() and abort_unless() throw, and how does the status code reach the error response?

level: juniorimportance: should knowfreq 52%

answer

  1. they throw, never return
  2. 404 gets NotFoundHttpException
  3. other codes get Symfony's HttpException
  4. message and headers ride along
  5. a Response argument becomes HttpResponseException

basics

~20 s

abort(404) throws Symfony's NotFoundHttpException and any other code throws HttpException with that status, message and headers; abort_if() and abort_unless() throw only when the condition is true or false. The handler renders the status's error view or JSON.

solid answer

~40 s

`abort($code, $message = '', array $headers = [])` never returns. For `404` it throws Symfony's `NotFoundHttpException`; for any other integer it throws `HttpException` with that status, message and headers. `abort_if($condition, ...)` and `abort_unless($condition, ...)` take the same arguments after a boolean and throw only when it is true or false respectively. The exception travels up to Laravel's handler, which reads `getStatusCode()` and `getHeaders()`: for HTML it renders `errors/{status}.blade.php` (or a fallback) with the exception as `$exception`, for JSON it returns `{"message": ...}` with that status. You can also pass a `Response` or `Responsable` instead of a code: `abort()` then throws an `HttpResponseException`, and that exact response is returned. Because it throws, code after a firing `abort()` never runs.

code

php · 18 lines
php
<?php

namespace App\Http\Controllers;

use App\Models\Flight;

class CheckInController extends Controller
{
    public function show(string $code)
    {
        $flight = Flight::where('code', $code)->first();

        abort_if($flight === null, 404, 'Flight not found.');
        abort_unless($flight->check_in_open, 409, 'Check-in is not open yet.');

        return view('check-in.show', ['flight' => $flight]);
    }
}

go deeper

for a junior

Know that abort(404) stops the request with a 404 and that abort_if/abort_unless are the conditional forms.

for a middle

Explain the exception each call throws, how status, message and headers reach the HTML or JSON response, and the Response-argument form.

for a senior

Keep abort messages user-safe, avoid swallowing HttpException in broad catches, and choose between abort() and explicit responses.

for a principal

Set conventions for when services may abort with HTTP semantics versus throwing domain exceptions the handler maps.

## What abort() is `abort()` is a global helper for stopping a request with an HTTP error from anywhere: a controller, a middleware, a service or a Blade view. Its signature in Laravel 13 is: ```php abort($code, $message = '', array $headers = []); ``` It never returns normally. Depending on `$code` it throws one of three exceptions: | Argument | Exception thrown | |---|---| | `404` | `Symfony\Component\HttpKernel\Exception\NotFoundHttpException` | | any other integer, for example `403`, `409`, `503` | `Symfony\Component\HttpKernel\Exception\HttpException` with that status | | a `Symfony\Component\HttpFoundation\Response` | `Illuminate\Http\Exceptions\HttpResponseException` wrapping it | | a `Responsable` object | `HttpResponseException` wrapping `toResponse(request())` | The integer cases go through `app()->abort()`, and `NotFoundHttpException` is itself a subclass of `HttpException`. ## The conditional forms Two helpers save an `if`: - `abort_if($condition, $code, $message = '', $headers = [])` aborts when the condition is **true**; - `abort_unless($condition, $code, $message = '', $headers = [])` aborts when it is **false**. Both are declared `void` and simply call `abort()` when they fire, so they throw the same exceptions. ## How the status reaches the response Nothing is sent at the moment `abort()` is called. The exception unwinds the call stack until Laravel's exception handler catches it, and the handler builds the response from the exception: 1. **Status code:** `getStatusCode()` on the `HttpException` becomes the response status. 2. **Headers:** the `$headers` array is attached to the response, which is how you send `Retry-After` with a 503 or 429. 3. **Body, HTML:** the handler renders `resources/views/errors/{status}.blade.php`, or a `4xx`/`5xx` fallback, passing the exception as `$exception`, so a view can print your `$message`. 4. **Body, JSON:** when the request should get JSON, the body is `{"message": "..."}` with the exception's message, even in production, because HTTP exception messages are treated as safe for clients. For the `HttpResponseException` case none of that applies: the handler returns the wrapped response unchanged, with no error view. ## Using it well - Write the message for the end user. It is shown in JSON bodies and wherever an error view prints it, so `abort(404, 'Flight not found.')` is fine and `abort(500, $e->getMessage())` is not. - Prefer `abort_unless($flight->check_in_open, 409, 'Check-in is not open yet.')` over nested `if` blocks in controllers; it reads as a guard clause. - For "does this user own this booking" checks, authorization through gates and policies is the idiomatic tool; `abort(403)` is the blunt version. - Do not put `abort()` inside a `try` block that catches `Throwable`, or your own `catch` swallows the HTTP exception before the handler sees it. - Pass headers the client needs, such as `Retry-After`, through the third argument rather than building a response by hand. ## Picking the status code The status you pass is the contract with the client, so choose it deliberately: | Code | Use it when | Airline example | |---|---|---| | 400 | The request itself is malformed | An unparseable date in a search query | | 403 | The user is known but not allowed | Opening another passenger's boarding pass | | 404 | The thing does not exist, or should not be admitted to exist | An unknown flight code | | 409 | The request conflicts with current state | Checking in before check-in opens | | 410 | The thing existed but is permanently gone | A fare that has been withdrawn | | 429 | The client is sending too much | Seat-map polling beyond the limit | | 503 | The service is temporarily unavailable | The seat inventory system is being updated | Each code maps to an error view by name (`errors/409.blade.php`) or to the `4xx`/`5xx` fallback, so branded pages follow automatically. Invalid form input is a separate case with its own 422 response from the validator, and is not something to `abort()` by hand. ## abort() compared with returning a response `return response()->json([...], 409);` from a controller also produces a 409, but only from that method. `abort()` works from deep inside services, where you cannot return a response to the router, and it lets the central handler decide between HTML and JSON and apply your error views, render callbacks and `respond()` hook. The trade-off is that exception-driven flow is less explicit; keep `abort()` for real error exits, not ordinary branching.

  • Is the abort() message shown to users in production?
    Yes, for HTTP exceptions. In a JSON response the body is `{"message": ...}` with your message even when `APP_DEBUG` is false, and error views can print it via `$exception->getMessage()`. Write it for the end user.
  • What happens if abort() is called inside a try block that catches Throwable?
    Your own `catch` receives the `HttpException` and, unless you rethrow it, the handler never sees it, so no error response is produced. Catch narrower exception types, or rethrow `HttpException`.

saying these in an interview costs you the question

  • abort() returns a response that the controller must return
  • abort(404) and abort(403) both throw NotFoundHttpException
  • Code after a firing abort_if() still runs until the method returns
  • abort() only accepts an integer status code
  • The abort() message is hidden from JSON clients in production