Upgrade to Pro — share decks privately, control downloads, hide ads and more …

phpDay 2019 - Handling Exceptional Conditions with Grace and Style

phpDay 2019 - Handling Exceptional Conditions with Grace and Style

Programmers naturally give more attention to a “happy path” - default scenario in application execution in which everything works as expected, therefore neglecting the opposite way things can go. Topics such as dealing with exceptional conditions, use of exceptions, error handling seem to have been insufficiently studied, and it is very difficult to find useful explanations and tutorials online.

This talk is an in-depth study about practices for dealing with exceptional conditions that promote code that is clean, consistent and convenient to work with. Special attention is given to applicable best practices for managing exceptions in a proper way, including formatting exception messages using named constructors, component-level exception type, exception wrapping.

To make the story complete, second part of the talk introduces a solution for establishing central error handling system that makes this critical aspect of the software stable, unambiguous and easy to maintain.

At the very end, some attention is given to testing exceptions and ways for keeping test code consistent and readable.

Nikola Poša

May 11, 2019
Tweet

More Decks by Nikola Poša

Other Decks in Programming

Transcript

  1. HANDLING EXCEPTIONAL CONDITIONS
    HANDLING EXCEPTIONAL CONDITIONS
    WITH GRACE AND STYLE
    WITH GRACE AND STYLE
    Nikola Poša · @nikolaposa
    phpDay · 11 May 2019

    View Slide

  2. Sono contento di essere qui
    Sono contento di essere qui

    View Slide

  3. Sono contento di essere qui
    Sono contento di essere qui
    ABOUT ME
    ABOUT ME
    Software Architect specializing in PHP-based
    applications
    Lead Architect at Arbor Education Partners
    PHP Serbia Conference co-organizer
     @nikolaposa
     blog.nikolaposa.in.rs

    View Slide

  4. AGENDA
    AGENDA
    Approaches for dealing with exceptional
    conditions
    Set of applicable best practices for managing
    exceptions in a proper way
    Solution for establishing central error handling
    system
    Few tips for testing exceptions

    View Slide

  5. HAPPY PATH
    HAPPY PATH
    a.k.a. Normal Flow
    Happy path is a default scenario
    featuring no exceptional or error
    conditions, and comprises nothing if
    everything goes as expected.
    Wikipedia

    View Slide

  6. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }

    View Slide

  7. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    Fatal error: Call to a member function isSubscribedTo() on null

    View Slide

  8. $user = $userRepository->get('John');
    if (null !== $user && $user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }

    View Slide

  9. $user = $userRepository->get('John');
    if (null !== $user && $user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    interface UserRepository
    {
    public function get(string $username): ?User;
    }

    View Slide

  10. DO NOT MESS WITH
    DO NOT MESS WITH
    NULL
    NULL

    View Slide

  11. When we return null, we are
    essentially creating work for
    ourselves and foisting problems upon
    our callers.
    Robert C. Martin, "Clean Code"

    View Slide

  12. If you are tempted to return null
    from a method, consider throwing
    an exception or returning a
    Special Case object instead.
    Robert C. Martin, "Clean Code"

    View Slide

  13. THROW MEANINGFUL EXCEPTION
    THROW MEANINGFUL EXCEPTION
    interface UserRepository
    {
    /**
    * @param string $username
    * @throws UserNotFound
    * @return User
    */
    public function get(string $username): User;
    }
    final class DbUserRepository implements UserRepository
    {
    public function get(string $username): User
    {
    $userRecord = $this->db->fetchAssoc('SELECT * FROM users W
    if (false === $userRecord) {
    throw new UserNotFound();
    }
    return User::fromArray($userRecord);
    }
    }

    View Slide

  14. interface UserRepository
    {
    @throws UserNotFound
    public function get(string $username): User;
    }

    View Slide

  15. try {
    $user = $userRepository->get($username);
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    } catch (UserNotFound $ex) {
    $this->logger->notice('User was not found', ['username' => $u
    }

    View Slide

  16. try {
    $user = $userRepository->get($username);
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    } catch (UserNotFound $ex) {
    $this->logger->notice('User was not found', ['username' => $u
    }
    try {
    $this->notifyUserIfSubscribed($username, $notification);
    } catch (\Throwable $ex) {
    $this->log($ex);
    }

    View Slide

  17. SPECIAL CASE
    SPECIAL CASE
    a.k.a. Null Object
    A subclass that provides special
    behavior for particular cases.
    Martin Fowler, "Patterns of Enterprise Application
    Architecture"

    View Slide

  18. class UnknownUser extends User
    {
    public function username(): string
    {
    return 'unknown';
    }
    public function isSubscribedTo(Notification $notification): b
    {
    return false;
    }
    }

    View Slide

  19. class UnknownUser extends User
    {
    public function username(): string
    {
    return 'unknown';
    }
    public function isSubscribedTo(Notification $notification): b
    {
    return false;
    }
    }
    final class DbUserRepository implements UserRepository
    {
    public function get(string $username): User
    {
    $userRecord = $this->db->fetchAssoc('SELECT * FROM users W
    if (false === $userRecord) {
    return new UnknownUser();
    }
    return User::fromArray($userRecord);
    }
    }

    View Slide

  20. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }

    View Slide

  21. Checking for Special Case

    View Slide

  22. Checking for Special Case
    if ($user instanceof UnknownUser) {
    //do something
    }

    View Slide

  23. Checking for Special Case
    if ($user instanceof UnknownUser) {
    //do something
    }
    if ($user === User::unknown()) {
    //do something
    }

    View Slide

  24. Special Case factory
    class User
    {
    public static function unknown(): User
    {
    static $unknownUser = null;
    if (null === $unknownUser) {
    $unknownUser = new UnknownUser();
    }
    return $unknownUser;
    }
    }

    View Slide

  25. Special Case object as private nested class
    class User
    {
    public static function unknown(): User
    {
    static $unknownUser = null;
    if (null === $unknownUser) {
    $unknownUser = new class extends User {
    public function username(): string
    {
    return 'unknown';
    }
    public function isSubscribedTo(Notification $noti
    {
    return false;
    }
    };
    }
    return $unknownUser;
    }
    }

    View Slide

  26. Returning null from methods is
    bad, but passing null into methods
    is worse.
    Robert C. Martin, "Clean Code"

    View Slide

  27. class Order
    {
    public function __construct(
    Product $product,
    Customer $customer,
    ?Discount $discount
    ) {
    $this->product = $product;
    $this->customer = $customer;
    $this->discount = $discount;
    }
    }
    final class PremiumDiscount implements Discount
    {
    public function apply(float $productPrice): float
    {
    return $productPrice * 0.5;
    }
    }

    View Slide

  28. ?Discount $discount
    if (null !== $this->discount) {
    $price = $this->discount->apply($price);
    }
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    12
    public function total(): float
    13
    {
    14
    $price = $this->product->getPrice();
    15
    16
    17
    18
    19
    20
    return $price;
    21
    }
    22
    }
    23

    View Slide

  29. Discount $discount
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    }
    12

    View Slide

  30. Discount $discount
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    }
    12
    final class NoDiscount implements Discount
    {
    public function apply(float $productPrice): float
    {
    return $productPrice;
    }
    }
    $order = new Order($product, $customer, new NoDiscount());

    View Slide

  31. EXCEPTION VS SPECIAL CASE
    EXCEPTION VS SPECIAL CASE
    Special Case as default strategy instead of
    optional parameters
    Exceptions break normal ow to split business
    logic from error handling
    Special Case handles exceptional behaviour
    Exception emphasizes violated business rule

    View Slide

  32. USING EXCEPTIONS
    USING EXCEPTIONS

    View Slide

  33. Should be simple as:
    throw new \Exception('User was not found by username: ' . $usernam

    View Slide

  34. Should be simple as:
    throw new \Exception('User was not found by username: ' . $usernam

    View Slide

  35. CUSTOM EXCEPTION TYPES
    CUSTOM EXCEPTION TYPES
    bring semantics to your code
    emphasise exception type instead of exception
    message
    allow caller to act di erently based on
    Exception type

    View Slide

  36. STRUCTURING EXCEPTIONS
    STRUCTURING EXCEPTIONS
    src/
    Todo/
    Exception/
    Model/
    User/
    Exception/
    Model/

    View Slide

  37. CREATING EXCEPTION CLASSES
    CREATING EXCEPTION CLASSES
    src/
    User/
    Exception/
    InvalidUsername.php
    UsernameAlreadyTaken.php
    UserNotFound.php
    final class UserNotFound extends \Exception
    {
    }

    View Slide

  38. use App\User\Exception\UserNotFoundException;
    try {
    throw new UserNotFoundException();
    } catch (UserNotFoundException $exception) {
    }

    View Slide

  39. use App\User\Exception\UserNotFoundException;
    try {
    throw new UserNotFoundException();
    } catch (UserNotFoundException $exception) {
    }
    use App\User\Exception\UserNotFound;
    try {
    throw new UserNotFound();
    } catch (UserNotFound $exception) {
    }

    View Slide

  40. COMPONENT LEVEL EXCEPTION
    COMPONENT LEVEL EXCEPTION
    TYPE
    TYPE

    View Slide

  41. COMPONENT LEVEL EXCEPTION
    COMPONENT LEVEL EXCEPTION
    TYPE
    TYPE
    namespace App\User\Exception;
    interface ExceptionInterface
    {
    }
    final class InvalidUsername extends \Exception implements
    ExceptionInterface
    {
    }
    final class UserNotFound extends \Exception implements
    ExceptionInterface
    {
    }

    View Slide

  42. use GuzzleHttp\Exception\ClientException;
    use GuzzleHttp\Exception\ServerException;
    use GuzzleHttp\Exception\GuzzleException; //marker interface
    try {
    //code that can emit exceptions
    } catch (ClientException $ex) {
    //...
    } catch (ServerException $ex) {
    //...
    } catch (GuzzleException $ex) {
    //...
    }

    View Slide

  43. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES

    View Slide

  44. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES
    throw new UserNotFound(sprintf(
    'User was not found by username: %s',
    $username
    ));

    View Slide

  45. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES
    throw new UserNotFound(sprintf(
    'User was not found by username: %s',
    $username
    ));
    throw new InsufficientPermissions(sprintf(
    'You do not have permission to %s %s with the id: %s',
    $privilege,
    get_class($entity),
    $entity->getId()
    ));

    View Slide

  46. Encapsulate formatting into Exception classes
    final class UserNotFound extends \Exception implements ExceptionI
    {
    public static function byUsername(string $username): self
    {
    return new self(sprintf(
    'User was not found by username: %s',
    $username
    ));
    }
    }

    View Slide

  47. Named Constructors communicate the intent
    throw UserNotFound::byUsername($username);

    View Slide

  48. Named Constructors communicate the intent
    throw UserNotFound::byUsername($username);
    throw TodoNotOpen::triedToSetDeadline($deadline, $this->status);
    throw TodoNotOpen::triedToMarkAsCompleted($this->status);

    View Slide

  49. PROVIDE CONTEXT
    PROVIDE CONTEXT

    View Slide

  50. PROVIDE CONTEXT
    PROVIDE CONTEXT
    final class UserNotFound extends \Exception implements ExceptionI
    {
    private $username;
    public static function byUsername(string $username): self
    {
    $ex = new self(sprintf('User was not found by username: %
    $ex->username = $username;
    return $ex;
    }
    public function username(): string
    {
    return $this->username;
    }
    }

    View Slide

  51. EXCEPTION WRAPPING
    EXCEPTION WRAPPING

    View Slide

  52. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    try {
    return $this->toResult(
    $this->httpClient->request('GET', '/users')
    );
    } catch (ConnectException $ex) {
    throw ApiNotAvailable::reason($ex);
    }

    View Slide

  53. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    try {
    return $this->toResult(
    $this->httpClient->request('GET', '/users')
    );
    } catch (ConnectException $ex) {
    throw ApiNotAvailable::reason($ex);
    }
    final class ApiNotAvailable extends \Exception implements Exceptio
    {
    public static function reason(ConnectException $error): self
    {
    return new self(
    'API is not available',
    0,
    $error //preserve previous error
    );
    }
    }

    View Slide

  54. RETROSPECT
    RETROSPECT
    1. create custom, cohesive Exception types
    2. introduce component-level exception type
    3. use Named Constructors to encapsulate
    message formatting and express the intent
    4. capture & provide the context of the
    exceptional condition
    5. apply exception wrapping to rethrow more
    informative exception

    View Slide

  55. ERROR HANDLING
    ERROR HANDLING

    View Slide

  56. WHEN TO CATCH EXCEPTIONS?
    WHEN TO CATCH EXCEPTIONS?

    View Slide

  57. WHEN TO CATCH EXCEPTIONS?
    WHEN TO CATCH EXCEPTIONS?
    Do NOT catch exceptions
    unless you have a very good reason

    View Slide

  58. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER
    Wraps the entire system to handle any uncaught
    exceptions from a single place

    View Slide

  59. CHALLENGES
    CHALLENGES
    user experience
    security
    logging

    View Slide

  60. CHALLENGES
    CHALLENGES
    user experience
    security
    logging
    adaptability

    View Slide

  61. EXISTING SOLUTIONS
    EXISTING SOLUTIONS

    View Slide

  62. EXISTING SOLUTIONS
    EXISTING SOLUTIONS
    - stack-based error handling, pretty
    error page, handlers for di erent response
    formats (JSON, XML)
    Whoops

    View Slide

  63. EXISTING SOLUTIONS
    EXISTING SOLUTIONS
    - stack-based error handling, pretty
    error page, handlers for di erent response
    formats (JSON, XML)
    - di erent formatting strategies
    (HTML, JSON, CLI), logging handler, non-
    blocking errors
    Whoops
    BooBoo

    View Slide

  64. Using Whoops
    final class ErrorHandlerFactory
    {
    public function __invoke(ContainerInterface $container)
    {
    $whoops = new \Whoops\Run();
    if (\Whoops\Util\Misc::isAjaxRequest()) {
    $whoops->pushHandler(new JsonResponseHandler());
    } elseif (\Whoops\Util\Misc::isCommandLine()) {
    $whoops->pushHandler(new CommandLineHandler());
    } else {
    $whoops->pushHandler(new PrettyPageHandler());
    }
    $whoops->pushHandler(new SetHttpStatusCodeHandler());
    $whoops->pushHandler(new LogHandler($container->get('Logg
    return $whoops;
    }
    }

    View Slide

  65. src/bootstrap.php
    public/index.php
    bin/app
    //... initialize DI container
    $container->get(\Whoops\Run::class)->register();
    return $container;
    $container = require __DIR__ . '/../src/bootstrap.php';
    $container->get('App\Web')->run();
    $container = require __DIR__ . '/../src/bootstrap.php';
    $container->get('App\Console')->run();

    View Slide

  66. Logging errors
    final class LogHandler extends Handler
    {
    public function handle()
    {
    $error = $this->getException();
    if ($error instanceof DontLog) {
    return self::DONE;
    }
    $this->logger->error($error->getMessage(), [
    'exception' => $error,
    ]);
    return self::DONE;
    }
    }

    View Slide

  67. final class UserNotFound extends \Exception implements
    ExceptionInterface,
    DontLog
    {
    //...
    }

    View Slide

  68. Setting HTTP status code
    final class SetHttpStatusCodeHandler extends Handler
    {
    public function handle()
    {
    $error = $this->getException();
    $httpStatusCode =
    ($error instanceof ProvidesHttpStatusCode)
    ? $error->getHttpStatusCode()
    : 500;
    $this->getRun()->sendHttpCode($httpStatusCode);
    return self::DONE;
    }
    }

    View Slide

  69. interface ProvidesHttpStatusCode
    {
    public function getHttpStatusCode(): int;
    }
    final class UserNotFound extends \Exception implements
    ExceptionInterface,
    DontLog,
    ProvidesHttpStatusCode
    {
    //...
    public function getHttpStatusCode(): int
    {
    return 404;
    }
    }

    View Slide

  70. The OCP (Open­Closed Principle) is
    one of the driving forces behind the
    architecture of systems. The goal is
    to make the system easy to extend
    without incurring a high impact of
    change.
    Robert C. Martin, "Clean Architecture"

    View Slide

  71. TEST EXCEPTIONAL
    TEST EXCEPTIONAL
    BEHAVIOUR
    BEHAVIOUR
    a.k.a. Negative Testing

    View Slide

  72. TESTING EXCEPTIONS WITH
    TESTING EXCEPTIONS WITH
    PHPUNIT
    PHPUNIT
    class TodoTest extends TestCase
    {
    /**
    * @test
    */
    public function it_throws_exception_on_reopening_if_incomplet
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN());
    $this->expectException(CannotReopenTodo::class);
    $todo->reopen();
    }
    }

    View Slide

  73. ARRANGE-ACT-ASSERT
    ARRANGE-ACT-ASSERT
    1. initialize SUT/prepare inputs
    2. perform action
    3. verify outcomes

    View Slide

  74. class TodoTest extends TestCase
    {
    /**
    * @test
    */
    public function it_gets_completed()
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN());
    $todo->complete();
    $this->assertTrue($todo->isCompleted());
    }
    }

    View Slide

  75. /**
    * @test
    */
    public function it_throws_exception_on_reopening_if_incomplete()
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN());
    try {
    $todo->reopen();
    $this->fail('Exception should have been raised');
    } catch (CannotReopenTodo $ex) {
    $this->assertSame(
    'Tried to reopen todo, but it is not completed.',
    $ex->getMessage()
    );
    }
    }

    View Slide

  76. Thank you
    Thank you
    Drop me some feedback and make this
    presentation better
    ·
    @nikolaposa blog.nikolaposa.in.rs

    View Slide