Laravel Apiable provides a Handler class that converts any PHP exception into a properly formatted JSON:API error response. Validation errors, authentication failures, and HTTP exceptions are each handled appropriately with the correct status codes.
In Laravel 11 and later, register the renderable inside bootstrap/app.php:
use OpenSoutheners\LaravelApiable\Support\Facades\Apiable;
use Throwable;
return Application::configure(basePath: dirname(__DIR__))
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (Throwable $e, $request) {
if ($request->is('api/*') && app()->bound('apiable')) {
return Apiable::jsonApiRenderable($e);
}
});
})->create();
For older applications, add the renderable inside the register() method of your exception handler:
use OpenSoutheners\LaravelApiable\Support\Facades\Apiable;
use Throwable;
public function register(): void
{
$this->renderable(function (Throwable $e, $request) {
if ($request->is('api/*') && app()->bound('apiable')) {
return Apiable::jsonApiRenderable($e);
}
});
}
Apiable::jsonApiRenderable()Apiable::jsonApiRenderable(Throwable $e, ?bool $withTrace = null): Handler
Creates a Handler instance that implements Responsable. The returned object can be returned directly from a renderable closure — Laravel will call toResponse() on it.
| Parameter | Type | Description |
|---|---|---|
$e |
Throwable |
The exception to convert. |
$withTrace |
bool|null |
Include stack trace in the response. Defaults to config('app.debug'). Pass true or false to override. |
The Handler class inspects the exception type and chooses the appropriate behaviour:
Illuminate\Validation\ValidationException is unpacked field-by-field. Each validation message becomes a separate error object with a source.pointer referencing the failed field:
{
"errors": [
{
"title": "The title field is required.",
"source": { "pointer": "title" },
"status": "422"
},
{
"title": "The body must be at least 10 characters.",
"source": { "pointer": "body" },
"status": "422"
}
]
}
Any exception implementing Symfony\Component\HttpKernel\Exception\HttpExceptionInterface (including Laravel's abort() helpers) uses the exception's own status code. Response headers from the exception are forwarded automatically.
Illuminate\Auth\AuthenticationException is mapped to HTTP 401 Unauthorized, since Laravel's native exception does not implement HttpExceptionInterface.
Generic exceptions produce an HTTP 500 Internal Server Error. When app.debug is false (or $withTrace is false), the message is replaced with the generic text "Internal server error." and the trace is omitted. In debug mode the real message and stack trace are included.
Illuminate\Database\QueryException also includes the database error code in the code field when trace is enabled.
{
"errors": [
{
"title": "Unauthenticated.",
"status": "401"
}
]
}
Each error object may contain:
| Field | Description |
|---|---|
title |
Short, human-readable summary of the error. |
detail |
Longer explanation (optional). |
source.pointer |
JSON pointer to the field that caused the error (validation only). |
status |
HTTP status code as a string. |
code |
Application-specific error code (query errors in debug mode). |
trace |
Stack trace array (only when debug mode is enabled). |
The Handler instance returned by jsonApiRenderable() exposes withHeader() for adding headers to the final response:
$exceptions->renderable(function (Throwable $e, $request) {
if ($request->is('api/*')) {
return Apiable::jsonApiRenderable($e)
->withHeader('X-Error-Id', (string) Str::uuid());
}
});
Headers from HttpExceptionInterface exceptions (such as WWW-Authenticate on a 401) are always merged in automatically.
JsonApiException: stacking multiple errorsYou can build a response with multiple error objects programmatically using JsonApiException:
use OpenSoutheners\LaravelApiable\JsonApiException;
$exception = new JsonApiException();
$exception->addError(
title: 'Invalid value for filter.',
detail: 'The "status" filter only accepts: draft, published.',
source: 'filter[status]',
status: 422,
);
$exception->addError(
title: 'Unknown sort field.',
source: 'sort',
status: 400,
);
throw $exception;
addError() signaturepublic function addError(
string $title,
?string $detail = null,
?string $source = null,
?int $status = 500,
int|string|null $code = null,
array $trace = []
): void
getErrors() / toArray()$exception->getErrors(); // returns the raw errors array
$exception->toArray(); // returns ['errors' => [...]]
{% hint style="info" %}
JsonApiException extends Exception, so it can be thrown anywhere in your application and caught by the registered renderable.
{% endhint %}
Pass true as the second argument to always include the stack trace, regardless of app.debug:
return Apiable::jsonApiRenderable($e, withTrace: true);
Pass false to always suppress it:
return Apiable::jsonApiRenderable($e, withTrace: false);
{% hint style="warning" %}
Never expose stack traces in production. Only pass withTrace: true in controlled environments such as staging or local debugging.
{% endhint %}
How can I help you explore Laravel packages today?