DEV Community

loading...

Demystifying Exceptions

Sergii Shymko
Director of Engineering, Magento Architect, Magento 2 Co-founder
Updated on ・5 min read

A solid understanding of exceptions is crucial to writing reliable programs. This article aims to lift the veil of mystery surrounding peculiar aspects of exception handling by unraveling their inner workings to familiar concepts.

The code examples and some nuances are specific to PHP, but the principles are transferable to other languages — particularly JavaScript.

Introduction

Exceptions are a special language mechanism designated for error handling. It relieves programmers of the need to reinvent ad hoc error formats and means for error carryover across the call stack. Without support on the language level, source code would be muddled with tedious error pass-through boilerplate.

Exceptions do not necessarily mean errors, although these words are often used interchangeably. They represent negative scenarios in an otherwise optimistic execution flow. In a broader sense, they’re exceptional situations defined by a programmer as worthy distinguishing in a particular domain.

Let the syntax guide us in unwinding the exception-handling behavior.

throw

The throw operator emits an exception that stops the normal sequential execution flow of a program, passing control to the exception-handling mechanism.

Common way to trigger an exception:

throw new \Exception;

Resist the temptation to ascribe extra significance to the throw new expression. Keywords throw and new belong to independent paradigms (exceptions and OOP, respectively). Exceptions just happen to be object-oriented and throw works on exception objects carrying debug information.

Here’s a more verbose equivalent:

$e = new \Exception();
throw $e;

Exceptions are often created inline right before use, but that doesn’t have to always be the case. An exception object can be instantiated, passed around, and kept in memory arbitrarily long before being thrown or even discarded.

Exceptions are ordinary objects with properties and methods. They can use inheritance to form hierarchies like other classes. There’s one unique trait: Exceptions must extend a class, such as \Exception, that can be thrown.

PHP 7 features \Throwable interface implemented by both \Exception and \Error classes, with the latter replacing the error-handling mechanism.

Whether you can locate a source code that triggered an exception or not, assume there’s a throw statement somewhere in there. See official docs on exceptions thrown by internal functions, as you cannot view their sources.

try..catch

Surrounding code with the try..catch block allows to handle exceptions that may be thrown during its execution at any call depth. The blocks can be nested directly or indirectly. Exceptions “bubble up” from their origin to the closest catch matching the exception type.

Basic exception-handling syntax:

try {
    doSomethingDangerous();  // throw new \Exception;
} catch (\Exception $e) {
    // ...
}

The caught exception can be either handled in full or processed partially and rethrown to give higher-level blocks a chance to participate in its handling. The empty catch block essentially suppresses or “swallows” an exception. Once fully handled, the execution proceeds to the next statement following the try..catch resuming the normal control flow of a program.

In the worst-case scenario, the exception remains unhandled and the built-in handler halts the program after outputting the debug information. Think of it as a try..catch surrounding the entire program, whose implementation echo $e; exit(255); is deliberately hidden from your eyes.

Handling of multiple exception types:

try { 
    doSomethingDangerous();
} catch (\InvalidArgumentException $e) { 
    // ...
} catch (\LogicException $e) { 
    // ...
} catch (\RuntimeException $e) { 
    // ...
}

The behavior of multiple catch clauses and dependency on declaration order can be understood using the following equivalent transformation:

try { 
    doSomethingDangerous();
} catch (\Exception $e) { 
    if ($e instanceof \InvalidArgumentException) { 
        // ...
    } else if ($e instanceof \LogicException) { 
        // ...
    } else if ($e instanceof \RuntimeException) { 
        // ...
    } else { 
        throw $e; 
    }
}

Alternative branches are mutually exclusive following the else if analogy. Exceptions of irrelevant types propagate further, as implied by throw $e.

The declaration order is important, as the evaluation stops at the first match. Parent-child relations between exception classes are carefully considered. For instance, if the \LogicException clause was declared first, it would match the subtype \InvalidArgumentException, rendering its specialized handler unreachable. Moving \RuntimeException around wouldn’t make a difference in this example, as it’s unrelated to \LogicException in the class hierarchy.

It is up to a programmer to decide which types of exceptions to handle on a particular application layer and which ones to pass through. Frameworks impose an error-handling philosophy to guide such decisions.

Rethrow

Rethrowing a caught exception is fundamentally no different from initially throwing an exception. The syntax is the same, except that throw is now called from within catch, which does have access to the exception instance. A brand new exception can be thrown instead of rethrowing the caught one.

One question arises, though: Will subsequent catch blocks of the same try construct that had caught an exception also catch a rethrown one?

try { 
    doSomethingDangerous();
} catch (\InvalidArgumentException $e) { 
    // ...
    throw new \LogicException();
} catch (\LogicException $e) { 
    // Will it catch the exception thrown above? 
    // ...
} catch (\RuntimeException $e) { 
    // ...
}

No, they won’t, as evident from the rewritten equivalent:

try { 
    doSomethingDangerous();
} catch (\Exception $e) { 
    if ($e instanceof \InvalidArgumentException) { 
        // ...
        throw new \LogicException(); 
    } else if ($e instanceof \LogicException) { 
        // Will it catch the exception thrown above? Nope 
        // ...
    } else if ($e instanceof \RuntimeException) { 
        // ...
    } else { 
        throw $e; 
    }
}

try..finally

The try..finally construct is used to specify the finalization logic to execute regardless of whether an exception occurs or not:

doSomethingSafe();
try { 
    doSomethingDangerous();
} finally { 
    undoSomethingSafe();
}

This is equivalent to the following try..catch:

doSomethingSafe();
try { 
    doSomethingDangerous(); 
    undoSomethingSafe();
} catch (\Throwable $e) { 
    undoSomethingSafe(); 
    throw $e;
}

As you can see, in the general case, the “undo” logic has to be duplicated. However, when a rethrow is not intended, the finalization logic can go after the try..catch without resorting to finally:

doSomethingSafe();
try { 
    doSomethingDangerous();
} catch (\Throwable $e) { 
    // Handle without throwing exceptions...
}
undoSomethingSafe();

try..finally..return

Besides the copy-paste relief, finally is useful in conjunction with return.

doSomethingSafe();
try { 
    return getSomethingDangerous();
} finally { 
    undoSomethingSafe();
}

This equivalent implementation reveals what’s going on underneath:

doSomethingSafe();
try { 
    $result = getSomethingDangerous(); 
    undoSomethingSafe(); 
    return $result;
} catch (\Throwable $e) { 
    undoSomethingSafe(); 
    throw $e;
}

The finally version is more concise and doesn’t use an extra variable.

finally..return

The behavior of return inside of finally is somewhat unintuitive.

try {
    return getSomethingDangerous();
} finally {
    return 'value returned no matter what';
}

The result of the call will always be discarded and any exceptions silenced!

Rewriting the code without finally makes it clear that the execution will never reach return $result or throw $e statements:

try { 
    $result = getSomethingDangerous(); 
    return 'value returned no matter what'; 
    return $result;
} catch (\Throwable $e) { 
    return 'value returned no matter what'; 
    throw $e;
}

The finally..return construct indicates a coding mistake to be avoided.


Pass on the exception-handling insights to your fellow programmers!

Discussion (0)

Forem Open with the Forem app