Laravel Custom Exception Handlers

Laravel throws a large number of exceptions and is very easy to customize them to suit our needs as required.

Note: These exceptions are most helpful if you’re building an API.

The code for common exception handler lies in the {project_root}/app/Exceptions/ folder, named Handler.php. The default file looks like this in Laravel 5.7.

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * @param  \Exception  $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }
}

Some of the common exceptions that we’re going to handle in this tutorial are AuthenticationException, MethodNotAllowedHttpException, ModelNotFoundException, NotAcceptableHttpException, NotFoundHttpException, PostTooLargeException, ValidationException.

Below is the explanation for each variable, functions and handler implemented.

protected $dontReport = [] is the array of exceptions that Laravel wouldn’t report. Here we’re going to mention the exceptions that we don’t want laravel to report.

/**
 * A list of the exception types that should not be reported.
 *
 * @var array
 */
protected $dontReport = [
    AuthenticationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Symfony\Component\HttpKernel\Exception\HttpException::class,
    ModelNotFoundException::class,
    \Illuminate\Session\TokenMismatchException::class,
    ValidationException::class,
];

Next is protected $dontFlash which holds the variables for which we don’t want values to be shown when the exception is being report. Most common of these are password or authorization token.

/**
 * A list of the inputs that are never flashed for validation exceptions.
 *
 * @var array
 */
protected $dontFlash = [
    'password',
    'password_confirmation',
];

Next we define ourselves the structure of json that we’d like to return in case any of the above exception occurs. For this we create an $errorObj variable and initErrorObj function that defines the structure.

/**
 * @var stdClass
 */
private $errorObj;

/**
 * setup basic error Obj with main properties
 *
 * Optional properties that can be added:
 * - details = [];
 * - innererror = [];
 *
 * @return stdClass
 */
private function initErrorObj()
{
    $this->errorObj = new \stdClass();
    $this->errorObj->code = "";
    $this->errorObj->message = "";
    $this->errorObj->target = "";
}

Next we configure the exception render method which will check if there exists a function for the raised exception and do the needful.

/**
 * Render an exception into an HTTP response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Exception  $exception
 * @return \Illuminate\Http\Response
 */
public function render($request, Exception $exception)
{
    $this->initErrorObj();

    $reflect = new \ReflectionClass($exception);
    $method = 'handle' . $reflect->getShortName();

    if (method_exists($this, $method)) {
        $this->errorObj->code = $reflect->getShortName();

        return $this->$method($exception)->header('Access-Control-Allow-Origin', '*');
    }

    return parent::render($request, $exception);
}

Authentication Exception

Whenever an user tries to make an unauthorized request, an AuthenticationException is raised by Laravel. Over here if the request is made via API call requesting JSON response we return a JSON response otherwise redirect the user to login page.

/**
 * Convert an authentication exception into an unauthenticated response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Auth\AuthenticationException  $exception
 * @return \Illuminate\Http\Response 401
 */
protected function unauthenticated($request, AuthenticationException $exception)
{
    if ($request->isJson() || $request->expectsJson()) {
        $this->errorObj = new \stdClass();
        $this->errorObj->code = "AuthenticationException";
        $this->errorObj->message = $exception->getMessage();
        $this->errorObj->message .= " You are not authorised to make this request.";
        $this->errorObj->target = "query";
        
        return response()->json(['error' => $this->errorObj], 401);
    }

    return redirect()->guest('login');
}

Method Not Allowed Http Exception

This is when the route exists for the request made but the request type does not. Say making a post request when a get request is required would end up throwing MethodNotAllowedHttpException.

/**
 * MethodNotAllowedHttpException
 * The used HTTP method is not allowed on this route in the API
 *
 * @param MethodNotAllowedHttpException $exception
 * @return \Illuminate\Http\Response 405
 */
protected function handleMethodNotAllowedHttpException(MethodNotAllowedHttpException $exception)
{
	$this->errorObj->message = "The used HTTP method is not allowed on this route in the API.";
	$this->errorObj->target = "query";
	
	return response()->json(['error' => $this->errorObj], 405);
}

Model Not Found Exception

This is when a Model is missing. Wonder how this would happen?

/**
 * ModelNotFoundException
 * The model is not found with given identifier
 * https://restpatterns.mindtouch.us/HTTP_Status_Codes/422_-_Unprocessable_Entity
 *
 * @param ModelNotFoundException $exception
 * @return \Illuminate\Http\Response 422
 */
protected function handleModelNotFoundException(ModelNotFoundException $exception)
{
	$fullmodel = $exception->getModel();
	$choppedUpModel = explode('\\', $fullmodel);
	$cleanedUpModel = array_pop($choppedUpModel);
	$this->errorObj->message = $cleanedUpModel . " model is not found with given identifier.";
	$this->errorObj->target = $cleanedUpModel;
	
	return response()->json(['error' => $this->errorObj], 422);
}

Not Acceptable Http Exception

This is when the route and method are both correct but the headers mis-match. Preferably Accept or Content-type headers not appropriately set for an API would throw NotAcceptableHttpException.

/**
 * NotAcceptableHttpException
 * The used HTTP Accept header is not allowed on this route in the API
 *
 * @param NotAcceptableHttpException $exception
 * @return \Illuminate\Http\Response 406
 */
protected function handleNotAcceptableHttpException(NotAcceptableHttpException $exception)
{
	$this->errorObj->message = "The used HTTP Accept header is not allowed on this route in the API.";
	$this->errorObj->target = "query";
	
	return response()->json(['error' => $this->errorObj], 406);
}

Not Found Http Exception

This is when a request is made to a route that does not exist for the API hence throwing NotFoundHttpException with 404 http response code.

/**
 * NotFoundHttpException
 * The requested path could not match a route in the API
 *
 * @param NotFoundHttpException $exception
 * @return \Illuminate\Http\Response 404
 */
protected function handleNotFoundHttpException(NotFoundHttpException $exception)
{
	$this->errorObj->message = "The requested path could not match a route in the API.";
	$this->errorObj->target = "query";
	
	return response()->json(['error' => $this->errorObj], 404);
}

Post Too Large Exception

This is when a size of request is too large for the API to handle. Usually occurs with multipart/form-data request sending large files resulting in PostTooLargeException.

/**
 * PostTooLargeException
 * The used HTTP method is not allowed on this route in the API
 *
 * @param PostTooLargeException $exception
 * @return \Illuminate\Http\Response 413
 */
protected function handlePostTooLargeException(PostTooLargeException $exception)
{
	$this->errorObj->message = "The payload size too large for the server to handle.";
	$this->errorObj->target = "request";
	
	return response()->json(['error' => $this->errorObj], 413);
}

Validation Exception

One of the most use functionality of Laravel is it’s validation module. A single place to format all validation messages is here. Here is an example showing how to customize error message format with ValidationException.

/**
 * ValidationException
 * Parameters did not pass validatio
 * https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#examples
 *
 * @param ValidationException $exception
 * @return \Illuminate\Http\Response 400
 */
protected function handleValidationException(ValidationException $exception)
{
    $this->errorObj->message = "Parameters did not pass validation";
    $this->errorObj->target = "parameters";
    $this->errorObj->details = [];

    foreach ($exception->validator->errors()->getMessages() as $field => $message) {
        $details = new \stdClass();
        $details->code = "NotValidParameter";
        $details->message = $message[0];
        $details->target = $field;
        if ($field === 'Channel') {
            $details->code = "ParentChildMismatch";
            $details->message = "The requested service did not find a match for the given channel identifier";
        }
        $this->errorObj->details[] = $details;
    }

    return response()->json(['error' => $this->errorObj], 400);
}

The original code for these Laravel Exception was written by Github user Stad Gent and can be found here.

Abhishek Gupta
Follow me
Latest posts by Abhishek Gupta (see all)

Leave a Reply