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!
Top comments (0)