PHP Language Basics

Errors And Exceptions Basics

Failure is part of a program's behavior. The important question is not whether something can go wrong, but whether the code detects the failure at the right boundary and responds without hiding the cause or exposing sensitive details.

PHP reports different kinds of problems. A syntax error means the file cannot be parsed. A warning reports a suspicious runtime condition but may allow execution to continue. An Error object represents serious engine-level failures such as many type errors. An exception is a Throwable object deliberately thrown by code when normal execution cannot continue along the current path.

Do not use these terms interchangeably. A warning is not automatically catchable with try and catch, and an expected business state is not automatically exceptional.

Validate Before Performing Risky Work

Prevent avoidable warnings by checking data before reading or operating on it:

PHP example
<?php

$customer = ['email' => 'ada@example.com'];

if (!isset($customer['name'])) {
    echo "Name is missing\n";
    return;
}

echo $customer['name'], PHP_EOL;

The script chooses the missing-key path deliberately instead of reading an absent key and relying on PHP's warning output.

Validation and exceptions solve different parts of the problem. Validation identifies an invalid value. Throwing an exception communicates that the current function cannot fulfill its contract with that value.

Throw When A Function Cannot Return Its Promised Result

This normalizer promises a non-empty string:

PHP example
<?php

function normaliseName(string $name): string
{
    $name = trim($name);

    if ($name === '') {
        throw new InvalidArgumentException('Name must not be empty.');
    }

    return $name;
}

Returning '' would violate the intended contract. Printing an error inside the function would not give the caller a usable string. throw stops the current path and sends a typed failure outward.

Use a specific built-in exception when its meaning fits. InvalidArgumentException communicates that a caller supplied an unacceptable argument. RuntimeException is often used when valid input cannot be processed because of a runtime condition. Custom exception classes become useful when callers need to distinguish application-specific failures; they are covered later with object-oriented PHP.

Catch At A Boundary That Can Respond

The caller can translate the exception into a controlled response:

PHP example
<?php

try {
    $name = normaliseName('   ');
    echo "Customer: $name\n";
} catch (InvalidArgumentException $exception) {
    echo "Please enter a customer name.\n";
}

Output:

Please enter a customer name.

When normaliseName() throws, PHP stops the remaining statements in the try block and searches for the first compatible catch. The echo after the function call does not run.

Catch where you can make a real decision: show a safe validation message, retry an operation under a defined policy, translate the failure for another layer, add context and rethrow, or terminate a command with an appropriate exit code.

Do not catch an exception merely to ignore it. Silent failure leaves the program in an unexplained state.

Exceptions Bubble Through Function Calls

A catch does not need to be in the function that throws:

PHP example
<?php

function parsePositiveQuantity(string $input): int
{
    if (!ctype_digit($input)) {
        throw new InvalidArgumentException('Quantity must contain digits only.');
    }

    $quantity = (int) $input;

    if ($quantity < 1) {
        throw new InvalidArgumentException('Quantity must be positive.');
    }

    return $quantity;
}

function buildQuantityLabel(string $input): string
{
    $quantity = parsePositiveQuantity($input);

    return "Quantity: $quantity";
}

try {
    echo buildQuantityLabel('0'), PHP_EOL;
} catch (InvalidArgumentException $exception) {
    echo "Invalid quantity\n";
}

The exception leaves parsePositiveQuantity(), then leaves buildQuantityLabel(), and reaches the matching catch. This movement up the call stack is often called bubbling.

If no compatible catch or global exception handler is found, the program terminates with an uncaught exception error. That is preferable to pretending the operation succeeded, but applications should establish controlled top-level handling.

Catch Specific Types Before Broad Types

A catch handles objects compatible with its declared type:

PHP example
<?php

try {
    echo parsePositiveQuantity('two'), PHP_EOL;
} catch (InvalidArgumentException $exception) {
    echo "The quantity input is invalid.\n";
} catch (RuntimeException $exception) {
    echo "The quantity could not be processed.\n";
}

Place specific catches before broader parent types. Catching Throwable can intercept both exceptions and engine Error objects, but doing so deep in application code can conceal programming defects such as a TypeError.

A top-level framework or command runner may catch Throwable to log the failure and produce one safe response. That is different from claiming every failure is recoverable.

Preserve The Original Failure When Adding Context

Sometimes a lower-level exception needs a more useful application meaning. Catch it, create a new exception with context, and pass the original as the previous exception rather than discarding it:

PHP example
try {
    $quantity = parsePositiveQuantity($input);
} catch (InvalidArgumentException $exception) {
    throw new RuntimeException("Could not build the order.", 0, $exception);
}

The outer message describes the failed operation, while the exception chain preserves the original validation cause for logs and debugging. Only add this layer when the new context helps a caller. Catching and immediately rethrowing the same exception without context usually adds no value.

Use finally For Cleanup That Must Be Attempted

A finally block runs after try and any matching catch, whether or not an exception was thrown:

PHP example
<?php

function demonstrateCleanup(bool $fail): void
{
    try {
        echo "Start\n";

        if ($fail) {
            throw new RuntimeException('Operation failed.');
        }

        echo "Complete\n";
    } catch (RuntimeException $exception) {
        echo "Handled failure\n";
    } finally {
        echo "Cleanup\n";
    }
}

demonstrateCleanup(true);

Output:

Start
Handled failure
Cleanup

Real cleanup includes releasing locks, restoring a temporary handler, or closing a manually managed resource. Avoid returning or throwing a new exception from finally; it can replace the result or obscure an earlier failure.

Do Not Confuse Expected Outcomes With Exceptions

An order being pending is an ordinary business state:

PHP example
<?php

function canShip(string $status): bool
{
    return $status === 'paid';
}

The caller can branch on false. No contract was broken.

An empty value passed to a function that promises a required name is different: the function cannot produce its promised result. That can justify an exception.

Use normal return values for expected alternatives that callers routinely inspect. Use exceptions for failures that interrupt the requested operation and should travel to a handling boundary.

Keep Internal Details Away From Users

An exception object can expose its message, code, file, line, and stack trace. Those details help developers, but raw output can reveal filesystem paths, configuration, queries, or other internals.

At a boundary, separate developer diagnostics from the public response:

PHP example
<?php

try {
    $quantity = parsePositiveQuantity('abc');
    echo "Accepted: $quantity\n";
} catch (InvalidArgumentException $exception) {
    error_log($exception->getMessage());
    echo "Please provide a positive whole number.\n";
}

In a real application, use its configured logger rather than scattered error_log() calls. The principle is stable: record enough context privately and return a safe, actionable message publicly.

Never place passwords, access tokens, or complete sensitive payloads in exception messages or logs.

Warnings Are A Separate Mechanism

Many PHP operations report warnings rather than throwing exceptions. A surrounding try block does not automatically convert those warnings into exceptions.

PHP provides set_error_handler() and ErrorException for controlled conversion of certain non-fatal errors, but that is application infrastructure, not a substitute for checking expected conditions. It also cannot handle every fatal error category.

For beginner code, prefer APIs and validation that make failure explicit. Check array keys, validate input types and ranges, and test operation results where the API documents a failure return value.

CLI Programs Need Failure Exit Codes

A command-line entry point should report failure and exit non-zero:

PHP example
<?php

try {
    $quantity = parsePositiveQuantity($argv[1] ?? '');
    echo "Quantity: $quantity\n";
} catch (InvalidArgumentException $exception) {
    fwrite(STDERR, "Invalid quantity\n");
    exit(1);
}

Standard output is for the normal result. Standard error is for diagnostics. Exit code 0 means success; a non-zero code lets shell scripts and deployment tools detect failure.

Keep exit() at the application boundary. Reusable functions should throw or return a result rather than terminating the entire process themselves.

Common Error-Handling Mistakes

Symptom Likely cause
Statement after a failing call never runs Exception left the try block immediately
Exception remains uncaught Catch type does not match or no boundary handles it
Programming defect disappears Broad Throwable catch swallowed it
User sees paths or stack traces Internal exception details were printed publicly
Warning appears despite try Operation reports warnings rather than exceptions
Program claims success after failure Exception was caught and ignored
CLI automation misses the failure Command printed an error but exited with code 0
Earlier exception is obscured finally returned or threw another failure
Validation code is duplicated everywhere Contract has no clear owning function

What You Should Be Able To Do

After this lesson, you should be able to distinguish validation, warnings, Error objects, and exceptions; throw InvalidArgumentException when a function cannot satisfy its input contract; trace how an exception bubbles; catch a specific type at a useful boundary; explain why later try statements are skipped; use finally for cleanup; keep expected business alternatives as return values; separate internal diagnostics from safe user messages; and return a non-zero CLI exit code on failure.

Official references: PHP's manual pages for exceptions, Throwable, and set_error_handler().

Practice

Task: Reject Empty Name

Task

Write this function:

function normaliseName(string $name): string

It must trim the input, throw InvalidArgumentException with the message Name is required. when the cleaned value is empty, and otherwise return the cleaned name.

Call the function separately with ' Ada ' and ' '. Wrap each call in its own try / catch so the second failure does not prevent the first result from being shown.

Print safe output rather than the exception message. Expected output:

Name: Ada
Invalid name

Hints

  • Validate the cleaned value, not the original string.
  • Throw before the successful return.
  • Catch InvalidArgumentException, not every Throwable.
Show solution

Solution

PHP example
<?php

function normaliseName(string $name): string
{
    $name = trim($name);

    if ($name === '') {
        throw new InvalidArgumentException('Name is required.');
    }

    return $name;
}

try {
    echo 'Name: ' . normaliseName(' Ada ') . "\n";
} catch (InvalidArgumentException $exception) {
    echo "Invalid name\n";
}

try {
    echo 'Name: ' . normaliseName('   ') . "\n";
} catch (InvalidArgumentException $exception) {
    echo "Invalid name\n";
}

Explanation

The function trims before checking, so a whitespace-only string becomes empty and violates the contract. The valid call returns Ada to its caller.

Each call has its own handling boundary. The second exception skips its echo expression and reaches the matching catch, which prints a stable public message rather than exposing the internal exception text.

Task: Predict Exception Path

Task

Predict the exact output before running this code:

PHP example
<?php

function requirePositiveQuantity(int $quantity): int
{
    echo "Validate $quantity\n";

    if ($quantity < 1) {
        throw new InvalidArgumentException('Quantity must be positive.');
    }

    echo "Accepted inside function\n";

    return $quantity;
}

try {
    echo "Before call\n";
    $quantity = requirePositiveQuantity(0);
    echo "Accepted: $quantity\n";
} catch (InvalidArgumentException $exception) {
    echo "Caught invalid quantity\n";
} finally {
    echo "Finished attempt\n";
}

echo "After try statement\n";

Identify which two echo statements are unreachable for this input, then run the unchanged code.

Hints

  • Throwing leaves both the function and the remaining try statements.
  • The matching catch runs before finally.
  • Execution continues after the complete try / catch / finally statement.
Show solution

Solution

PHP example
<?php

function requirePositiveQuantity(int $quantity): int
{
    echo "Validate $quantity\n";

    if ($quantity < 1) {
        throw new InvalidArgumentException('Quantity must be positive.');
    }

    echo "Accepted inside function\n";

    return $quantity;
}

try {
    echo "Before call\n";
    $quantity = requirePositiveQuantity(0);
    echo "Accepted: $quantity\n";
} catch (InvalidArgumentException $exception) {
    echo "Caught invalid quantity\n";
} finally {
    echo "Finished attempt\n";
}

echo "After try statement\n";

// Prints:
// Before call
// Validate 0
// Caught invalid quantity
// Finished attempt
// After try statement

Explanation

The call begins and prints Validate 0. The exception is thrown before Accepted inside function, so that line is unreachable. Control also leaves the remaining try block, so Accepted: 0 is not printed.

The compatible catch handles the exception. finally then runs, and normal execution resumes after the entire statement. This path shows that catching an exception does not restart the failed function call.

Task: Handle Invalid Amounts

Task

Write a function named parsePositiveAmount() that accepts a string and returns a positive integer.

The function must:

  • throw InvalidArgumentException('Amount must contain digits only.') if ctype_digit() is false;
  • cast valid digit text to an integer;
  • throw InvalidArgumentException('Amount must be positive.') when the integer is 0;
  • return the amount otherwise.

Loop over ['25', '0', '4.5', '12']. Put the function call for each value inside try / catch. Print Accepted: <amount> on success and Rejected: <input> for an expected validation exception.

Expected output:

Accepted: 25
Rejected: 0
Rejected: 4.5
Accepted: 12

Hints

  • Catch inside the loop so one invalid value does not stop later values.
  • ctype_digit('4.5') is false because the dot is not a digit.
  • Do not print the internal exception message as the public result.
Show solution

Solution

PHP example
<?php

function parsePositiveAmount(string $value): int
{
    if (!ctype_digit($value)) {
        throw new InvalidArgumentException('Amount must contain digits only.');
    }

    $amount = (int) $value;

    if ($amount === 0) {
        throw new InvalidArgumentException('Amount must be positive.');
    }

    return $amount;
}

$inputs = ['25', '0', '4.5', '12'];

foreach ($inputs as $input) {
    try {
        $amount = parsePositiveAmount($input);
        echo "Accepted: $amount\n";
    } catch (InvalidArgumentException $exception) {
        echo "Rejected: $input\n";
    }
}

Explanation

25 and 12 contain digits only and produce positive integers. 0 passes the digit check but fails the positive-value check. 4.5 fails the first check because its decimal point is not a digit.

The catch is inside the loop, so each input has an independent handling boundary. A rejected value does not prevent later values from being attempted. The exception type is specific, and the output does not reveal internal validation text.