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

Made with Slides.com