Laravel has different default exceptions. For example ValidationException, AuthenticationException, NotFoundHttpException, etc. Each exception can be handled and rendered for user by its own way. For example, NotFoundHttpException usually is converted to beautiful 404 page. It is not so bad, but when AuthenticationException is thrown, Laravel just redirect user to the login page. This may work well for web, but when it occurs for API routes, it can consume a significant amount of time for debugging.
In this article, I will explain how to return exception as JSON in Laravel for API routes and how to configure it globally in the exceptions handler.
How to Return Exception as JSON in Laravel
By default, Laravel only returns JSON when the request expects to receive JSON. You can try to send request to /api/user endpoint in Postman and you will get either the default login page if it is configured, or another exception with a not-so-helpful message.

If you wish to force Laravel return JSON response for exceptions and don’t want to change source code of application, simply add Accept header with the application/json value. For example, in Postman:

To set this header for your request collection, use the Pre-request scripts feature instead of manually adding it to each request. To do this, open the options menu for your collection and click on Edit:

Then, select the Pre-request Scripts tab and paste this code into it:
pm.request.headers.add({
key: "Accept",
value: "application/json",
});

After this, Postman will automatically add this header for each request for this collection.
If you want to resolve this issue at the application level, you can modify the Exceptions\Handler class. By default it contains nothing noteworthy. But it is extended from Illuminate\Foundation\Exceptions\Handler. However, it extends from Illuminate\Foundation\Exceptions\Handler, which has a method named shouldReturnJson() with the following contents:
protected function shouldReturnJson($request, Throwable $e)
{
return $request->expectsJson();
}
You can override this method in your exception handler and do something like this:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
//....
protected function shouldReturnJson($request, Throwable $e): bool
{
return parent::shouldReturnJson($request, $e) || $request->is("api/*");
}
}
In this code the default behavior is preserved and additionally a JSON response is returned for all requests that contain the “api/” string in their path. As result, Laravel will always return JSON response for API routes.
The default Laravel exceptions are excellent. They provide detailed messages and appropriate status codes. However, if you throw a custom exception and debugging is disabled, you will get only the message with the text “Server error” and the 500 status code. Laravel only outputs messages for classes which extend HttpException.
This is done for security reasons, to prevent users from seeing sensitive information about your application in a production environment. However, you may want to return a custom message for your response. For example, you could create a YourExcepiton class and throw it in any route:
class YourException extends \RuntimeException
{
}
throw new YourException("Your exception message");
You can cast your exception into a HttpException in the prepareResponse() method. Here its default code:
protected function prepareException(Throwable $e)
{
return match (true) {
$e instanceof BackedEnumCaseNotFoundException
=> new NotFoundHttpException($e->getMessage(), $e),
$e instanceof ModelNotFoundException => new NotFoundHttpException(
$e->getMessage(),
$e
),
$e instanceof AuthorizationException && $e->hasStatus()
=> new HttpException(
$e->status(),
$e->response()?->message() ?:
Response::$statusTexts[$e->status()] ??
"Whoops, looks like something went wrong.",
$e
),
$e instanceof AuthorizationException && !$e->hasStatus()
=> new AccessDeniedHttpException($e->getMessage(), $e),
$e instanceof TokenMismatchException => new HttpException(
419,
$e->getMessage(),
$e
),
$e instanceof SuspiciousOperationException => new NotFoundHttpException(
"Bad hostname provided.",
$e
),
$e instanceof RecordsNotFoundException => new NotFoundHttpException(
"Not found.",
$e
),
default => $e,
};
}
It casts default Laravel exceptions into NotFoundHttpException or AccessDeniedHttpException and returns exception unchanged if no match is found. For example, if you want to cast your exception into the “Not Found” response you can use this code:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
//....
protected function prepareException(Throwable $e)
{
if ($e instanceof YourException) {
return new NotFoundHttpException($e->getMessage());
} else {
return parent::prepareException($e);
}
}
}

Also, you can transform your exception into basic HttpException with custom status code:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
//....
protected function prepareException(Throwable $e)
{
if ($e instanceof YourException) {
return new HttpException(422, $e->getMessage());
} else {
return parent::prepareException($e);
}
}
}

Wrapping Up
In this article I have explained how to return exception as JSON in Laravel for API routes and how to customize the exception message and status code for your exception classes. Hope that was helpful for you!