Best practices for handling exceptional behavior

Nikola Poša

Web developer & Open-Source Contributor

Normal vs Exceptional flow

  • normal flow - one that would be executed in the absence of errors
  • exceptional flow - disrupts normal flow after some exceptional condition is met

Do NOT return null

  • don't foist callers with problems
  • throw an exception
  • return Special Case object

Do NOT return null

class ArticleService
{
    public function getArticle(string $id)
    {
        $article = $this->repository->findBy('id', $id).first();
        
        if (!$this->getPermissionsService()->can('read', $article)) {
            return null;
        }
        
        return $article;
    }
}

Do NOT return null

class ArticleService
{
    public function getArticle(string $id) : Article
    {
        $article = $this->repository->findBy('id', $id).first();
        
        if (!$this->getPermissionsService()->can('read', $article)) {
            throw new InsufficientPermissionsException();
        }
        
        return $article;
    }
}

throw an exception

Do NOT return null

class ArticleService
{
    public function getArticle(string $id)
    {
        $articles = $this->repository->findBy('id', $id);
        
        if ($articles.isEmpty()) {
            return null;
        }
        
        $article = $articles.first();
        
        if (!$this->getPermissionsService()->can('read', $article)) {
            return null;
        }
        
        return $article;
    }
}

Do NOT return null

class ArticleService
{
    /**
     * @param string $id
     * @throws ArticleNotFoundException
     * @throws InsufficientPermissionsException
     * @return Article
     */
    public function getArticle(string $id) : Article
    {
        $articles = $this->repository->findBy('id', $id);
        
        if ($articles.isEmpty()) {
            throw new ArticleNotFoundException();
        }
        
        $article = $articles.first();
        
        if (!$this->getPermissionsService()->can('read', $article)) {
            throw new InsufficientPermissionsException();
        }
        
        return $article;
    }
}

throw an exception

Do NOT return null

return Special Case object

special/custom version of a on object that is returned in a normal flow

public function getArticle(string $id) : Article
{
    $articles = $this->repository->findBy('id', $id);
    
    if ($articles.isEmpty()) {
        return new NonexistentArticle();
    }
    
    $article = $articles.first();
    
    return $article;
}
class Article
{
    //...
}

class NonexistentArticle extends Article
{
    public function getTitle()
    {
        return 'Not found';
    }
    
    public function getContent()
    {
        return 'Go back to <a href="/">Home Page</a>';
    }
}

Working with Exceptions


throw new \Exception('Article with the ID: ' . $id .  ' does not exist');

Custom Exception classes

  • improved readability/expressiveness
  • caller can act differently based on Exception type

Custom Exception classes

class ArticleNotFoundException extends RuntimeException
{
}

//...

throw new ArticleNotFoundException('Article with the ID: ' . $id .  ' does not exist');

Formatting Exception messages

  • rellocation of formatting logic
    • formatting logic in a place where exceptions are thrown results in distracting and noisy code
    • separation of responsibilities
  • named constructors
class ArticleNotFoundException extends RuntimeException
{
    public static function forId(string $id) : self
    {
        return new self(sprintf(
            'Article with the ID: %s does not exist',
            $id
        ));
    }
}

//...

throw ArticleNotFoundException::forId($id);

Formatting Exception messages

class InsufficientPermissionsException extends RuntimeException
{
    private $entity;
    
    private $privilege;
    
    public static function forEntityAndPrivilege(
        EntityInterface $entity, 
        string $privilege
    ) : self {
        $exception = new self(sprintf(
            'You do not have permission to %s %s with the id: %s',
            $privilege,
            get_class($entity),
            $entity->getId()
        ));
        
        $exception->entity = $entity;
        $exception->privilege = $privilege;
        
        return $exception;
    }
    
    public function getEntity() : EntityInterface
    {
        return $this->entity;
    }
    
    public function getPrivilege() : string
    {
        return $this->privilege;
    }
}

Cohesive Exception classes

  • Exception classes must not violate SRP

Cohesive Exception classes

class UserException extends Exception
{
    public static function forEmptyEmail() : self
    {
        return new self("User's email must not be empty");
    }
    
    public static function forInvalidEmail(string $email) : self
    {
        return new self(sprintf(
            '%s is not a valid email address',
            $email
        ));
    }
        
    public static function forNonexistentUser(string $userId) : self
    {
        return new self(sprintf(
            'User with the ID: %s does not exist',
            $userId
        ));
    }
}

Cohesive Exception classes

class InvalidUserException extends DomainException
{
    public static function forEmptyEmail() : self
    {
        return new self("User's email address must not be empty");
    }
    
    public static function forInvalidEmail(string $email) : self
    {
        return new self(sprintf(
            '%s is not a valid email address',
            $email
        ));
    }
}

class UserNotFoundException extends RuntimeException
{
    public static function forUserId(string $userId) : self
    {
        return new self(sprintf(
            'User with the ID: %s does not exist',
            $userId
        ));
    }
}

Component-level Exceptions

  • allows having Exception type that can be caught for any exception that emanates within a component
  • good practice in case of library code
  • accomplished by applying Marker Interface

Component-level Exceptions

namespace App\Domain\Exception;

interface ExceptionInterface
{
}

class UserNotFoundException extends \RuntimeException impements ExceptionInterface
{
    public static function forUserId(string $userId) : self
    {
        return new self(
            sprintf(
                'User with the ID: %s does not exist',
                $userId
            )
        );
    }
}

Component-level Exceptions

use App\DBAL\Exception\ExceptionInterface as DBALException;
use App\Domain\Exception\ExceptionInterface as DomainException;

try {
    //some code...
} catch (DBALException $dbalEx) {
    //...
} catch (DomainException $domainEx) {
    //...
} catch (\Exception $ex) {
    //...
}

Component-level Exceptions

Exception codes

  • uniquely identifies error that happened
  • typically used for APIs

Exception codes

class UserNotFoundException extends RuntimeException
{
    public static function forUserId(string $userId) : self
    {
        return new self(
            sprintf(
                'User with the ID: %s does not exist',
                $userId
            ), 
            ErrorCodes::ERROR_USER_NOT_FOUND
        );
    }
}

Error Handling

  • Domain layer freely raises exceptions
  • Global error handler above your code
    • Built-in framework mechanism
    • Popular error handling libraries (Whoops, Booboo)
  • PHP 7 Error exceptions

Error Handling

use Whoops\Run;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\JsonResponseHandler;

$run = new Run();

$run->pushHandler(new PrettyPageHandler());

if (\Whoops\Util\Misc::isAjaxRequest()) {
    $jsonHandler = new JsonResponseHandler();
    $run->pushHandler($jsonHandler);
}

$run->register();

Error Handling

Best practices for handling exceptional behavior

By Nikola Poša

Best practices for handling exceptional behavior

Overview of widely accepted best practices for dealing with exceptional situations, writing and organizing exception classes, as well as error handling concepts. Guideline for writing custom exception classes, formatting exception messages, component-level exceptions technique, and much more. In a word - Exceptions cheat-sheet.

  • 2,012