Tech is political: The people under attack in Palestine 🇵🇸, Iran 🇮🇷, and Lebanon 🇱🇧 are people like us. They’re our brothers and sisters, too. Read up on their history, scrutinize what you’re told, and demand that they be respected and included. Hide

Frontend Dogma

A Laravel Exception Context Solution

on , tagged , (share this post, e.g., on Mastodon or on Bluesky)

When an exception occurs, we usually want to show users a friendly message. However, the framework’s default exception handling approach is not ideal for this scenario.

Recently, we ran into a situation where, when a user lacked permission to access a piece of information, we wanted to display a detailed reason. For example, when a non-member attempts to access a team resource, we want to show something like:

You are not a member of team [xxxxxx], so you cannot view this resource at the moment. <Request Access>

At the same time, we need to display a masked team name and a button for requesting access. The API logic, however, simply called abort whenever the user lacked permission:

abort_if(!$user->isMember($resource->team), 403, 'You do not have permission to access this resource');

The response looks like this:

HTTP/1.0 403 Forbidden

{
  "message": "You do not have permission to access this resource"
}

We obviously cannot use the message field to render an HTML error page on the frontend. That would create excessive coupling and violate the principle of frontend/backend separation.

Our goal is simply to return a response in the following format:

HTTP/1.0 403 Forbidden

{
  "message": "You do not have permission to access this resource",
  "team": {
    "id": "abxT8sioa0Ms",
    "name": "CoDesign****"
  }
}

By including contextual data alongside the exception, frontend developers can freely compose the UI however they like.

Getting Started

Of course, this is not particularly complicated. We can solve it by modifying the original abort_if:

- abort_if(!$user->isMember($resource->team), 403, 'You do not have permission to access this resource');+ if (!$user->isMember($resource->team)) {
+  return response()->json([
+    'message' => 'You do not have permission to access this resource',
+    'team' => [
+      'id' => $resource->team_id,
+      'name'=> $resource->team->desensitised_name,
+    ]
+  ], 403);
+ }

At first glance, this seems to solve the problem. But consider what happens if the permission check occurs inside a closure and you need to exit early. This return-based approach becomes awkward because return only terminates the nearest execution context.

Ideally, we still want behavior similar to abort, which stops execution across the entire application flow. So let’s improve the implementation further.

Improving the Implementation

After looking at the source code for abort, I noticed that its first argument actually accepts an instance of \Symfony\Component\HttpFoundation\Response.

The response returned by our return statement above is exactly that type, so we can simply rewrite it like this:

if (!$user->isMember($resource->team)) {
  abort(response()->json([
    'message' => 'You do not have permission to access this resource',
    'team' => [
      'id' => $resource->team_id,
      'name'=> $resource->team->desensitised_name,
    ]
  ], 403));
}

That introduces a new problem, though. If this logic needs to be reused, things become awkward again. The same block of code will end up duplicated everywhere this permission check exists, which is not what we want.

Reusing the Logic

To make the logic reusable, I took a closer look at the implementation of \App\Exceptions\Handler and discovered that the parent class’s render method contains the following design:

public function render($request, Throwable $e)
{
    if (method_exists($e, 'render') && $response = $e->render($request)) {
        return Router::toResponse($request, $response);
    } elseif ($e instanceof Responsable) {
        return $e->toResponse($request);
    }

    // …
}

Because of this, we can extract the logic into a dedicated exception class and simply implement a render method:

$ ./artisan make:exception NotTeamMemberException

The code looks like this:

<?php

namespace App\Exceptions;

use App\Team;

class NotTeamMemberException extends \Exception
{
    public Team $team;

    public function __construct(Team $team, $message = "")
    {
        $this->team = $team;
        parent::__construct($message, 403);
    }

    public function render()
    {
        return response()->json(
            [
                'message' => !empty($this->message) ? $this->message : 'You do not have permission to access this resource',
                'team' => [
                    'id' => $this->team->id,
                    'name' => $this->team->desensitised_name,
                ],
            ],
            403
        );
    }
}

Now our logic becomes:

if (!$user->isMember($resource->team)) {
    throw new NotTeamMemberException($resource->team, 'You do not have permission to access this resource');
}

Or, more concisely:

\throw_if(!$user->isMember($resource->team), NotTeamMemberException::class, $resource->team, 'You do not have permission to access this resource');

At this point, the problem is solved in a fairly elegant way.

If you have a better approach, feel free to leave a comment and discuss it.

(This post is a machine-made, human-reviewed, and authorized translation of overtrue.me/­laravel-exception-with-context/­.)