Exceptions are an effective way to handle error conditions without adding too much "noise" to our code. In doing so, we will explore how Exceptions enable us to decouple the signaling of an error from the handling of it.
We will look at examples extrapolated from real-world scenarios, highlighting what our code looks like without leveraging exceptions, and what it could look like when leveraging them. We will also show consequences of insufficiently handling error conditions.
8. Validations: Painful
❖ if (
$this->validateThingOne() &&
$this->validateThingTwo() &&
$this->validateThingThree()
) {
doSomeTransaction();
return true; //signal that the thing was done
} else {
return false; //signal the thing was not done
}
9. Validations: Painful
❖ private function validateThingOne()
{
if (//someErrorCondition) {
return false;
} else {
return true;
}
}
10. Validations: Painful
❖ private function validateThingTwo()
{
if (//someErrorCondition) {
return false;
} else {
return true;
}
}
11. Validations: Painful
❖ private function validateThingThree()
{
if (//someErrorCondition) {
return false;
} else {
return true;
}
}
12. Validations: Painful
❖ Validation methods must “return true / false” …
❖ to signal success or failure
❖ upstream methods must adapt behavior …
❖ based on true / false
❖ Code has more nesting
17. Validations: Graceful
❖ private function validateThingOne()
{
if (//someErrorCondition) {
throw new InvalidThingOneException();
}
}
18. Validations: Graceful
❖ private function validateThingTwo()
{
if (//someErrorCondition) {
throw new InvalidThingTwoException();
}
}
19. Validations: Graceful
❖ private function validateThingThree()
{
if (//someErrorCondition) {
throw new InvalidThingThreeException();
}
}
20. Validations: Graceful
❖ Validation methods don’t need to return anything
❖ they only need to “throw Exception” on failure
❖ upstream methods don’t have to care
❖ Code has far less nesting.
28. getUserById(int $userId) : User
❖ public function getUserById(int $userId)
{
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
}
29. getUserById(int $userId) : User
❖ public function getUserById(int $userId)
{
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
}
30. getUserById(int $userId) : User
❖ public function getUserById(int $userId)
{
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
}
What if Bogus?
35. UserRepository
❖ public function getUserById(int $userId)
{
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
}
36. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
return null;
}
}
37. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
return null;
}
}
38. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
return null; //no Result was found, returning null
}
}
40. ProductsService
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return [];
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
41. ProductsService
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return [];
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
42. ProductsService
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return [];
}
//we don’t want to send a ‘null’ $user to getProducts
$products = $this->productsRepo->getProducts($user);
return $products;
}
43. ProductsService
❖ If no user is found …
❖ I return an empty array … of Products
❖ … because it feels convenient
❖ is this, however, elegant?
44. ProductsService
❖ Empty array of Products will be returned if:
❖ correct user, but user has no products
❖ no user was found
45. ProductsService
❖ Empty array of Products will be returned if:
❖ correct user, but user has no products <— That’s OK
❖ no user was found
46. ProductsService
❖ Empty array of Products will be returned if:
❖ correct user, but user has no products <— That’s OK
❖ no user was found
47. ProductsService
❖ Empty array of Products will be returned if:
❖ correct user, but user has no products <— That’s OK
❖ no user was found <— strange behavior
49. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return [];
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
50. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return [];
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
X
51. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return -1;
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
52. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return -1; //using -1 to “signal” no user found
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
53. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return -1; //using -1 to “signal” no user found
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
54. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return -1;
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
55. ProductsService: Alternative
❖ public function getProductsForUser(int $userId)
{
$user = $this->userRepo->getUserById($id);
if (is_null($user)) {
return -1;
}
$products = $this->productsRepo->getProducts($user);
return $products;
}
Inconsistent Return Types !
56. ProductsService: Alternative
❖ I return “-1” to “signal” that no user could be found
❖ so my Service can now either return:
❖ an array of products
❖ or -1
❖ … that’s not … “great”.
58. ProductsController
❖ Based on what the Service returns, the Controller has two
options:
❖ Option 1: also always return an array
❖ Option 2: handle “-1”
59. ProductsController: Option 1
❖ Option 1: ProductsService always returns an array
❖ ProductsController has no way …
❖ to “detect that no user was found”
❖ ProductsController sends-out empty array in all cases
❖ Consumer of ProductsController has no way to know
that they passed-in a wrong User ID
❖ and goes-on thinking that “user has no products”
60. ProductsController: Option 2
❖ Option 2: ProductsService returns -1 when no User found
❖ ProductsController must handle that special-case
❖ Send custom error message “User does not exist”
62. ProductsController: Option 1
❖ public function getProducts(int $userId)
{
return $this
->productsService
->getProductsForUser($userId);
}
63. ProductsController: Option 1
❖ public function getProducts(int $userId)
{
return $this
->productsService
->getProductsForUser($userId)
//if non-existent userId was passed
//empty array is still returned
}
64. ProductsController: Option 1
❖ public function getProducts(int $userId)
{
return $this
->productsService
->getProductsForUser($userId)
//if non-existent userId was passed
//empty array is still returned
//API consumer is confused :(
}
66. ProductsController: Option 2
❖ public function getProducts(int $userId)
{
return $this
->productsService
->getProductsForUser($userId);
}
67. ProductsController: Option 2
❖ public function getProducts(int $userId)
{
$products = $this
->productsService
->getProductsForUser($userId);
if ($products === -1) {
$this->sendError(‘Bad User ID: ’.$userId);
} else {
return $products;
}
}
68. ProductsController: Option 2
❖ public function getProducts(int $userId)
{
$products = $this
->productsService
->getProductsForUser($userId);
if ($products === -1) {
$this->sendError(‘Bad User ID: ’.$userId);
} else {
return $products;
}
}
69. ProductsController: Option 2
❖ Pros:
❖ We don’t confuse API consumer when they send a bad
User ID
❖ Cons:
❖ More Code
❖ Mixing User-related code with Products-related code
❖ Controller has to know what “-1” means !!!
71. Impacts
❖ UserRepository could be used by dozens of Services
❖ Each Service will have to “check for null” when no User
❖ And in turn, find a way to “Signal” this upstream …
❖ … to the Controllers with “-1”
❖ and Controllers will have to handle “-1”
❖ … every. time.
72. Impacts
❖ Now …
❖ Multiply the preceding challenges …
❖ By Dozens of Repositories
74. Impacts
❖ Rampant “Status Codes”: -1, -2, -3, -4
❖ to signal a myriad of error conditions
❖ bound to status messages …
❖ maintained in lookup arrays
75. Impacts
❖ So. Much. Code.
❖ Corners will be cut.
❖ Strange behavior will creep throughout the system.
78. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
return null;
}
}
79. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
return null;
}
}
X
80. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
}
}
82. UserNotFoundException
❖ class UserNotFoundException extends Exception
{
const MESSAGE = ‘User ID could not be found: ’;
public function __construct(string $userId = “”)
{
parent::__construct(
self::MESSAGE.intval($userId),
404,
null
);
}
}
89. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
}
}
102. Benefits of Exceptions
❖ Less code to handle error cases
❖ Localize signaling at component level
❖ Encapsulate more information
❖ Promote decoupling: upstream components don’t have to
“know” about downstream error cases
104. Exceptions Best Practices
❖ Catch ORM Exceptions and re-throw your own.
❖ Make “lots” of Custom Exception Classes
❖ Think about Exception hierarchy / taxonomy
❖ avoid reusing same exception w/ different error messages
❖ Embed messaging inside exception.
❖ Name them as specifically as possible.
❖ Include helpful messaging as to what happened.
❖ possibly escape or cast inputs - XSS vulnerabilities
❖ intval($userId)
107. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
}
}
108. UserRepository
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
}
}
117. Polymorphic Exceptions
❖ catch (NoResultException $nre) {
//specific handling of No Result
❖ } catch (UnexpectedResultException $ure) {
//fall-back to any UnexpectedResultException
//other than NoResultException
//which includes NonUniqueResultException
❖ } catch (ORMException $oe) {
//fall-back to any ORMException
//other than UnexpectedResultException
}
119. Polymorphic Exceptions
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
}
}
120. Polymorphic Exceptions
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException $e) {
throw new UserNotFoundException($userId);
} catch (UnexpectedResultException $ure) {
$this->log(‘This should not happen: ’.$ure);
throw new UnexpectedApplicationException();
}
}
122. Catching Exceptions with “|”
❖ Useful when trying to catch multiple exceptions …
❖ … When no common ancestor is available
123. Catching Exceptions with “|”
❖ public function getUserById(int $userId)
{
try {
return $this->em->createQuery(
‘select u from E:User u where u.id = :userId’
)
->setParameter(‘userId’, $userId)
->getSingleResult();
} catch (NoResultException | NonUniqueResultException $e) {
throw new UserNotFoundException($userId);
}
}
125. Exceptions & PHPUnit
❖ Declare that what comes next will trigger an Exception
❖ Trigger the Exception-throwing behavior
126. Exceptions & PHPUnit
❖ public function testWrongUserIdThrowsException
{
$bogusUserId = 4815162342;
$this->expectException(UserNotFoundException::class);
$this->userRepo->getUserById($bogusUserId);
}
127. Exceptions & PHPUnit
❖ public function testWrongUserIdThrowsException
{
$bogusUserId = 4815162342;
$this->expectException(UserNotFoundException::class);
$this->userRepo->getUserById($bogusUserId);
}
128. Exceptions & PHPUnit
❖ public function testWrongUserIdThrowsException
{
$bogusUserId = 4815162342;
//I am signaling that an exception will be thrown
$this->expectException(UserNotFoundException::class);
$this->userRepo->getUserById($bogusUserId);
}
129. Exceptions & PHPUnit
❖ public function testWrongUserIdThrowsException
{
$bogusUserId = 4815162342;
//I am signaling that an exception will be thrown
$this->expectException(UserNotFoundException::class);
//I am triggering the exception with $bogusUserId
$this->userRepo->getUserById($bogusUserId);
}
130. Exceptions & PHPUnit
❖ public function testWrongUserIdThrowsException
{
$bogusUserId = 4815162342;
//I am signaling that an exception will be thrown
$this->expectException(UserNotFoundException::class);
//I am triggering the exception with $bogusUserId
$this->userRepo->getUserById($bogusUserId);
}