Let's talk about something totally different: a powerful part of object-oriented
code called exceptions.

In index.php, we create a BrokenShip object. I'm going to do something crazy,
guys. I'm going to say, $brokenShip->setStrength() and pass it... banana:

146 lines index.php

... lines 1 - 4

useModel\BrokenShip;

... lines 6 - 13

$brokenShip = new BrokenShip('Just a hunk of metal');

$brokenShip->setStrength('banana');

... lines 16 - 146

That strength makes no sense. And if we try to battle using this ship, we should
get some sort of error. But when we refresh... well, it is an error: but not
exactly what I expected.

This error is coming from AbstractShip line 65. Open that up. I want you to look
at 2 exceptional things here:

125 lines lib/Model/AbstractShip.php

... lines 1 - 4

abstractclassAbstractShip

{

... lines 7 - 44

publicfunctionsetStrength($number)

{

if (!is_numeric($number)) {

thrownew \Exception('Strength must be a number, duh!');

}

$this->strength = $number;

}

... lines 53 - 123

}

First, we planned ahead. When we created the setStrength() method, we said:

You know what? This needs to be a number, so if somebody passes something
dumb like "banana," then let's check for that and trigger an error.

Second, in order to trigger an error, we threw an exception. And that's actually
what I want to talk about: Exceptions are classes, but they're completely special.

But first, Exception is a core PHP class, and when we added a namespace to this
file, we forgot to change it to \Exception:

125 lines lib/Model/AbstractShip.php

... lines 1 - 4

abstractclassAbstractShip

{

... lines 7 - 44

publicfunctionsetStrength($number)

{

if (!is_numeric($number)) {

thrownew \Exception('Strength must be a number, duh!');

}

$this->strength = $number;

}

... lines 53 - 123

}

That's better. Now refresh again. This is a much better error:

Uncaught Exception: Invalid strength passed: "banana"

When things go Wrong: Throw an Exception

When things go wrong, we throw exceptions. Why? Well, first: it stops execution of
the page and immediately shows us a nice error.

Tip

If you install the XDebug extension, exception messages are more helpful, prettier
and will fix your code for you (ok, that last part is a lie).

Catching Exceptions: Much Better than Catching a Cold

Second, exceptions are catchable. Here's what that means.

Suppose that I wanted to kill the page right here with an error. I actually have
two options: I can throw an exception, or I could print some error message and
use a die statement to stop execution.

But when you use a die statement, your script is truly done: none of your other
code executes. But with an exception, you can actually try to recover and keep
going!

Let's look at how. Open up PdoShipStorage. Inside fetchAllShipsData(), change the
table name to fooooo:

35 lines lib/Service/PdoShipStorage.php

... lines 1 - 4

classPdoShipStorageimplementsShipStorageInterface

{

... lines 7 - 13

publicfunctionfetchAllShipsData()

{

$statement = $this->pdo->prepare('SELECT * FROM FOOOOO');

... lines 17 - 19

}

... lines 21 - 33

}

That clearly will not work. This method is called by ShipLoader, inside getShips():

67 lines lib/Service/ShipLoader.php

... lines 1 - 8

classShipLoader

{

... lines 11 - 20

publicfunctiongetShips()

{

... lines 23 - 24

$shipsData = $this->queryForShips();

... lines 26 - 31

}

... lines 33 - 60

privatefunctionqueryForShips()

{

return$this->shipStorage->fetchAllShipsData();

}

}

When we try to run this, we get an exception:

Base table or view not found

The error is coming from PdoShipStorage on line 18, but we can also see the line
that called this: ShipLoader line 23.

Now, what if we knew that sometimes, for some reason, an exception like this might
be thrown when we call fetchAllShipsData(). And when that happens, we don't want
to kill the page or show an error. Instead, we want to - temporarily - render the
page with zero ships.

How can we do this? First, surround the line - or lines - that might fail with a
try-catch block. In the catch, add \Exception $e:

72 lines lib/Service/ShipLoader.php

... lines 1 - 8

classShipLoader

{

... lines 11 - 60

privatefunctionqueryForShips()

{

try {

return$this->shipStorage->fetchAllShipsData();

} catch (\Exception $e) {

... lines 66 - 67

}

}

}

Now, if the fetchAllShipsData() method throws an exception, the page will not die.
Instead, the code inside catch will be called and then execution will keep going like normal:

72 lines lib/Service/ShipLoader.php

... lines 1 - 8

classShipLoader

{

... lines 11 - 60

privatefunctionqueryForShips()

{

try {

return$this->shipStorage->fetchAllShipsData();

} catch (\Exception $e) {

// if all else fails, just return an empty array

return [];

}

}

}

That means, we can say $shipData = array().

Using the Exception Object

And just like that, the page works. That's the power of exceptions. When you throw
an exception, any code that calls your code has the opportunity to catch the exception
and say:

No no no, I don't want the page to die. Instead, let's do something else.

Of course, we probably also don't want this to fail silently without us knowing,
so you might trigger an error and print the message for our logs. Notice, in catch,
we have access to the Exception object, and every exception has a getMessage()
method on it. Use that to trigger an error to our logs:

73 lines lib/Service/ShipLoader.php

... lines 1 - 8

classShipLoader

{

... lines 11 - 60

privatefunctionqueryForShips()

{

try {

return$this->shipStorage->fetchAllShipsData();

} catch (\Exception $e) {

trigger_error('Exception! '.$e->getMessage());

// if all else fails, just return an empty array

return [];

}

}

}

Ok, refresh! Right now, we see the error on top of the page. But that's just because
of our error_reporting settings in php.ini. On production, this wouldn't display,
but would write a line to our logs.

Leave a comment!

2016-10-25Max

Wow! Thank you so much for this detailed and helpful explanation! I'm going to use the try-catch-possibility wisely ;)

2016-10-24weaverryan

Hey Max!

In practice, not really. But GREAT question - and I realize that are example isn't the best for answering this :). Most of the time I just call functions and *allow* them to throw an exception (which *will* cause an error on the page). The reason is that most exceptions are... quite exceptional and rare. In the rare cases that something crazy happens, I actually *do* want the exception to be thrown and "uncaught" so that the page has an error. Since I use Symfony, I configure my framework to send me messages (I do this via Slack) whenever there is an exception. That way, yes, one user might see an error (like the 502 error that you saw on the challenge) but I'm notified :). If you try/catch everything, and try to recover, even when something crazy is happening, it's not really a great policy.

In reality, you should use a try-catch when you know that you might call a method, and it might throw an exception under some reasonable/normal conditions. I'll give you 2 examples from our site :).

1) We use Stripe for ecommerce. When you talk to Stripe's API using their PHP library, and a credit card is declined, their library throws a Stripe\Card\Error exception. Since that's a normal/predictable situation, we catch that error and show the user a really nice error.

2) We use Guzzle in many places to make API requests to other sites. Occasionally, we make a request to a site and we *know* that the endpoint *might* return a 400 status code instead of 200 under normal conditions (the details why this is normal for us aren't important - the point is, we *expect* this behavior sometimes). Guzzle throws an exception when a 400 status is returned. So, we try-catch those calls so that we can take action when the status is 400.

So you really need to ask could calling this function under normal conditions result in a predictable exception? Or would an exception happen only in crazy situations. The example in this chapter would actually be a case where I would *not* catch the exception... unless you're having crazy database situations where you expect your database to fail routinely (which is not a great situation).

Cheers!

2016-10-24Max

So basically it is obligatory to try-catch every function that throws an exception?

It should work if you try it again :). It looks like the temporary machine we create for you had shutdown *right* as you answered the question (for security, the machines are temporary - we try to keep them alive, but they have a max life of 20 minutes). Sorry you hit that - I got a report in our logs about it actually - it happens occasionally (that a user submits *right* when it shuts down).