Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

depsimon's avatar

Best way to handle REST API errors thrown from Controller or Exception

Hello everyone,

I've been working on my API for weeks now and I'm still arguing myself about how to handle my errors in a good and maintainable way.

At some point in the Incremental API course, @JeffreyWay shows us a fine way to handle errors with the ApiController. (ie: respondWithError($message)).

Then later in the serie he shows us how to handle errors with the exceptions handler of Laravel which now is found in the app/Exceptions/Handler.php file (I'm using Laravel 5.0).

So that means that our code to respond with errors is duplicated and that bugs me.

How would you deal with this in an efficient way ? I'm thinking about a Trait or a Response Macro but I have no idea which is best..

0 likes
12 replies
taijuten's avatar

The way I do it is I have an APIcontroller which all of my controllers extend. Within this APIController I have methods to trigger exceptions, passing messages through where appropriate.

However, the actual handling of these exceptions is still dealt with in the exceptionhandler.

Doing this means you're separating triggering an exception and the action taken place when an exception occurs. This also means that if you have a package which triggers an exception outside your app folder, the exception is still handled with your exceptionhandler.

depsimon's avatar

@taijuten I've the same setup as you do: My ApiController is extended by my other Controllers and it sometimes throws Exceptions.

But when I capture these Exceptions (through the Exception Handler), I still want these to be rendered as a json object (like any other response rendered by my APIController). That means that either I handle my Exceptions like this :

// class App\Exceptions\Handler
public function render($request, Exception $e)
{
    if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
        return response()->json([
            'data' => [
                'message' => 'Resource not found',
                'status_code' => Response::HTTP_NOT_FOUND
            ]
        ], Response::HTTP_NOT_FOUND);
    } elseif ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
        return response()->json([
            'data' => [
                'message' => 'Endpoint not found',
                'status_code' => Response::HTTP_NOT_FOUND
            ]
        ], Response::HTTP_NOT_FOUND);
    }

    return response()->json([
            'data' => [
                'message' => $e->getMessage(),
                'status_code' => Response::HTTP_BAD_REQUEST
            ]
        ], Response::HTTP_BAD_REQUEST);
}

which is painful to read and maintain..

I'm currently going with a custom ResponseTrait that I use in my ApiController and in my Exception/Handler classes.

That allows me to update my code to this :

public function render($request, Exception $e)
{
    if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
        return $this->setStatusCode(Response::HTTP_NOT_FOUND)->respondWithError('Resource not found');
    } elseif ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
        return $this->setStatusCode(Response::HTTP_NOT_FOUND)->respondWithError('Endpoint not found');
    }

    return $this->setStatusCode(Response::HTTP_BAD_REQUEST)->respondWithError($e->getMessage());
}
```
crishellco's avatar
Level 3

My solution is to use a couple of traits in the <app name>\Exceptions\Handler class and add a few lines to the render method.

<?php

namespace App\Traits;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;

trait RestExceptionHandlerTrait
{

    /**
     * Creates a new JSON response based on exception type.
     *
     * @param Request $request
     * @param Exception $e
     * @return \Illuminate\Http\JsonResponse
     */
    protected function getJsonResponseForException(Request $request, Exception $e)
    {
        switch(true) {
            case $this->isModelNotFoundException($e):
                $retval = $this->modelNotFound();
                break;
            default:
                $retval = $this->badRequest();
        }

        return $retval;
    }

    /**
     * Returns json response for generic bad request.
     *
     * @param string $message
     * @param int $statusCode
     * @return \Illuminate\Http\JsonResponse
     */
    protected function badRequest($message='Bad request', $statusCode=400)
    {
        return $this->jsonResponse(['error' => $message], $statusCode);
    }

    /**
     * Returns json response for Eloquent model not found exception.
     *
     * @param string $message
     * @param int $statusCode
     * @return \Illuminate\Http\JsonResponse
     */
    protected function modelNotFound($message='Record not found', $statusCode=404)
    {
        return $this->jsonResponse(['error' => $message], $statusCode);
    }

    /**
     * Returns json response.
     *
     * @param array|null $payload
     * @param int $statusCode
     * @return \Illuminate\Http\JsonResponse
     */
    protected function jsonResponse(array $payload=null, $statusCode=404)
    {
        $payload = $payload ?: [];

        return response()->json($payload, $statusCode);
    }

    /**
     * Determines if the given exception is an Eloquent model not found.
     *
     * @param Exception $e
     * @return bool
     */
    protected function isModelNotFoundException(Exception $e)
    {
        return $e instanceof ModelNotFoundException;
    }

}
<?php

namespace App\Traits;

use Illuminate\Http\Request;

trait RestTrait
{

    /**
     * Determines if request is an api call.
     *
     * If the request URI contains '/api/v'.
     *
     * @param Request $request
     * @return bool
     */
    protected function isApiCall(Request $request)
    {
        return strpos($request->getUri(), '/api/v') !== false;
    }

}
<?php

namespace Attend\Exceptions;

use Exception;
use Attend\Traits\RestTrait;
use Attend\Traits\RestExceptionHandlerTrait;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{

    use RestTrait;
    use RestExceptionHandlerTrait;

...

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $e
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $e)
    {
        if(!$this->isApiCall($request)) {
            $retval = parent::render($request, $e);
        } else {
            $retval = $this->getJsonResponseForException($request, $e);
        }

        return $retval;
    }
}
23 likes
crishellco's avatar

@enginerd It is the same as the following. I was just thinking ahead a bit of other possible Model exceptions that I may want to handle.

if($this->isModelNotFoundException($e)) {
    $retval = $this->modelNotFound();
} else { 
    $retval = $this->badRequest();
}
vwasteels's avatar

Combining various ideas I have seen, this is my : App\Exceptions\Handler :

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

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

    /**
     * Report or log an exception.
     *
     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
     *
     * @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 $e)
    {
        if(!$request->expectsJson()) return parent::render($request, $e);
        
        switch(true) {
            case $e instanceof ModelNotFoundException:
                return response()->json([
                    'message' => 'Record not found',
                ], 404);
                break;
            case $e instanceof NotFoundHttpException:
                return response()->json([
                    'message' => 'Page not found',
                ], 404);
                break;
        }        
    }

    /**
     * Convert an authentication exception into an unauthenticated response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Auth\AuthenticationException  $exception
     * @return \Illuminate\Http\Response
     */
    protected function unauthenticated($request, AuthenticationException $exception)
    {
        if ($request->expectsJson()) {
            return response()->json(['error' => 'Unauthenticated.'], 401);
        }

        return redirect()->guest(route('login'));
    }
}
1 like
stkfinans's avatar

Here is what I ended up using, a bit more dynamic approach (maybe not suited for production):

    public function render($request, Exception $e)
    {
        if( $request->expectsJson() ) {
            $return = [
                "error" => [
                    "errors" => [
                        "file" => $e->getFile(),
                        "line" => $e->getLine(),
                        "exception" => (new \ReflectionClass($e))->getShortName(),
                    ],
                    "code" => $e->getCode(),
                    "message" => $e->getMessage()
                ]
            ];

            return response()->json($return, 404);
        }

        return parent::render($request, $e);
    }
afraz's avatar

trait ApiResponses{

 /**
 * Returns a success json response of given data.
 *
 * @param array $array
 * @param string $status_code
 * @return \Illuminate\Http\JsonResponse
 */

public function returnSuccessResponseArray($array = null,$status_code = 200)
{
    $array['status'] = $array['status'] ?? 'success';
    return response()->json($array,$status_code);
}

/**
 *  Returns a failed json response of given data.
 *
 * @param array $array
 * @param string $status_code
 * @return \Illuminate\Http\JsonResponse
 */
public function returnFailedResponseArray($array = null,$status_code = 400)
{
    $array['status'] = $array['status'] ?? 'error';
    return response()->json($array,$status_code);
}

/**
 * Returns exceptions details.
 * @param object $e
 * @return string
 */
public function errorDetails($e)
{
    if(app()->environment() == 'local'){
        return ' '.$e->getMessage() . ' ' . $e->getFile() . ' ' . $e->getLine();
    }
    return '';
}

/**
 * Returns exceptions details.
 *
 * @param object $e
 * @param string $message
 * @param string $route
 * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
 */
public function toDashboard($e,$message = null,$route = null)
{
    if(is_null($message)){
        $message = 'Something went wrong. Please try again. '.$this->errorDetails($e);
    }else{
        $message .=' '.$this->errorDetails($e);
    }

    if(is_null($route)){
        if(authUser()->isEmployer()){
            $route = 'employer.dashboard';
        }elseif(authUser()->isJobSeeker()){
            $route = 'job_seeker.dashboard';
        }
    }
    if(request()->wantsJson()){
        return $this->returnFailedResponseArray(['message' => $message]);
    }
    return redirect()->route($route)->with('error',$message);
}

}

mohammadMahdi's avatar

so nice! create specific exception for any kind of exception would lead us to cleaner code and more readable one

Please or to participate in this conversation.