Modern PHPExtra· 40 min read

Exceptions, Errors & Logging

Handle failure like a professional — custom exception classes, try/catch/finally, the difference between errors and exceptions, and writing problems to a log file.

What you will learn

  • Write and throw custom exception classes
  • Use try / catch / finally and catch multiple types
  • Log errors to a file and hide them from users in production

Errors vs exceptions

You met try/catch briefly. Now we go deeper, because robust apps depend on it. First, two words that sound the same but are not. An exception is a problem you can *anticipate and handle* — a missing file, a failed payment, invalid input. An error (in the old sense) is usually a *bug* in your code — a typo, calling a function that does not exist. Modern PHP turns most of these into exception-like objects, so you can catch them too.

The point of exceptions is control: instead of the whole page dying with an ugly message, you decide what happens when something goes wrong.

try / catch / finally

You wrap risky code in try. If it throws, control jumps to catch. The finally block — new to you — always runs afterwards, whether things succeeded or failed; it is where you clean up (close files, release resources).

try / catch / finally
<?php
  function divide(int $a, int $b): float {
    if ($b === 0) {
      throw new InvalidArgumentException("Cannot divide by zero");
    }
    return $a / $b;
  }

  try {
    echo divide(10, 0);
  } catch (InvalidArgumentException $e) {
    echo "Problem: " . $e->getMessage();
  } finally {
    echo " (done)";
  }
?>

Step through it: divide(10, 0) checks for a zero divisor and, finding one, throws an InvalidArgumentException with a message. That throw immediately stops the try and jumps to the catch, where $e->getMessage() reads back the message we set. Then finally runs no matter what — here it prints (done) — which is where you would close a database connection or file handle.

Note: Output (in the browser): Problem: Cannot divide by zero (done) Notice finally ran even though the code failed. If you called divide(10, 2) instead, you would see 5 (done) — the finally still runs on success. That guarantee is what makes it perfect for cleanup.

Custom exception classes

For real apps you create your own exception types by extending the built-in Exception class. This lets you catch exactly the kind of failure you care about and ignore the rest — far more precise than catching everything.

Custom exceptions, caught by type
<?php
  class PaymentFailedException extends Exception {}
  class OutOfStockException extends Exception {}

  function checkout(int $stock): string {
    if ($stock <= 0) {
      throw new OutOfStockException("That item is sold out");
    }
    return "Order placed!";
  }

  try {
    echo checkout(0);
  } catch (OutOfStockException $e) {
    echo "Sorry: " . $e->getMessage();
  } catch (PaymentFailedException $e) {
    echo "Payment issue: " . $e->getMessage();
  }
?>

We define two named exception types — they are empty classes that simply extends Exception, which is enough to give them their own identity. checkout(0) throws an OutOfStockException. Because we have a catch block specifically for that type, that block runs (and the unrelated PaymentFailedException catch is skipped). Each kind of failure gets its own tailored response.

Note: Output (in the browser): Sorry: That item is sold out Naming your exceptions makes the code read like the business problem — "out of stock", "payment failed" — and lets each be handled differently. This is exactly the pattern frameworks like Laravel use everywhere.

Logging instead of showing errors

In production (a live site) you must never show raw error details to visitors — they leak information and look broken. Instead you log the problem to a file for yourself and show the user a calm, generic message. error_log() appends a line to a log file.

Log the detail, show the user a safe message
<?php
  try {
    // ... risky work, e.g. a database call ...
    throw new Exception("DB connection refused");
  } catch (Exception $e) {
    // for YOU: full detail goes to a private log file
    error_log($e->getMessage(), 3, "errors.log");
    // for the USER: a friendly, vague message
    echo "Sorry, something went wrong. Please try again later.";
  }
?>

error_log($e->getMessage(), 3, "errors.log") writes the real error text to a file named errors.log (the 3 means "append to this file"). The visitor, meanwhile, only ever sees the polite sentence in the echo — never the technical cause. You read the log; they read reassurance.

Note: Output (in the browser): Sorry, something went wrong. Please try again later. Meanwhile a line like DB connection refused is quietly added to errors.log for you to investigate. This split — detail to the log, friendliness to the user — is a core production habit.

Watch out: On a live server, also set display_errors to off in php.ini so PHP never prints raw warnings to visitors, and keep log_errors on. We cover this in the deployment lesson.

Q. In production, what should happen when an exception is caught?

Answer: Log the technical detail privately (error_log) for yourself, and show visitors only a calm, generic message — never raw error text.

✍️ Practice

  1. Write a custom ValidationException and throw it from a function when input is bad.
  2. Add a finally block that prints a "cleanup" message whether the code succeeds or fails.

🏠 Homework

  1. Wrap a fake database call in try/catch that logs the real error with error_log and shows the user a safe message.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →