The try/throw/catch paradigm is a fundamental part of most programming languages, and it’s an early concept engineers learn. However, knowing about it doesn’t always mean we use it correctly. This article looks into the hidden dangers of inadequate error and exception handling — how silent failures and cryptic stack traces can undermine website performance and user satisfaction.
Many organizations focus on developing new features rather than addressing technical debt and legacy issues. Balancing growth and maintenance is always a challenge, but how do we know when to prioritize fixing bugs and uncovering hidden errors? It all starts with a useful, comprehensive error log.
Exceptions and errors
Despite its importance, exception handling is often treated as an afterthought. Developers might rely on default behaviors, sprinkle try/catch blocks haphazardly, or worse, ignore potential errors all together. This oversight can lead to silent failures, obscure bugs, and an overall lack of reliability in our applications.
So, what exactly are exceptions? In short, they are problems that are exceptional. In the early days of computing, exceptions were created to solve big problems — like hardware getting stuck in an infinite loop, causing major disruptions in timeshare systems. The spectrum of issues is vast, ranging from hardware malfunctions like a hard disk crash to simple programming mishaps like accessing an out-of-bounds array element. Exception handling interrupts the regular flow of a program when a major issue crops up. It’s about identifying the problem and ensuring someone can address it or at least acknowledge its existence.
Nowadays, we talk about error handling and exception handling interchangeably. They are different, but the lines between them are softening. Errors typically denote runtime issues that halt the program’s execution, often requiring a programming change to resolve. Syntax errors and logical blunders fall under this category.
On the other hand, exceptions can be managed and sometimes even predicted. They stem from code that is grammatically correct but faces unexpected conditions at runtime. Examples include attempting to access a nonexistent file or connectivity issues with an API. These are problems that we can recover from exceptions.
Mechanics of try/throw/catch/finally
What do I mean by “recover” from exceptions? Well, there’s a tried-and-true pattern for that, which exists in almost all programming languages.
- Try: This block is where you place the code to monitor for exceptions. Essentially, you’re saying, “Try to execute this code, and if something goes wrong, we’ll handle it.”
- Throw: Think of throwing an exception as a way for the code to say, “Hey, something unexpected happened, and I can’t handle it here!” This signal is passed through the layers of your program until it finds a piece of code that knows how to deal with it.
- Catch: If an exception occurs, our code jumps to the catch block for follow-up actions. This could include logging the error, cleaning up resources, displaying a user-friendly error message, or even re-throwing the exception to be handled at a higher level.
- Finally: The final block is always executed, regardless of whether an exception was thrown or not. This is often for cleanup tasks such as closing file handles, releasing database connections, or freeing up other resources.
Best practices
Log exceptions
Logging exceptions is crucial for diagnosing issues in production environments. Use a logging framework to capture detailed information about exceptions, including stack traces, to aid in troubleshooting.
function mymodule_user_insert(EntityInterface $entity) { try { // Send a request to the organization directory API... if(!$response->body()) { throw new Exception("Failed to connect to the API."); } // Add the directory information to the new user... } catch (Exception $e) { // Log the error without stopping the code execution… \Drupal::logger('mymodule')->error($e->getMessage()); } }
Resist the urge to handle exceptions immediately
Sometimes we can manage or work around failures. This is especially true when developing code that may be contributed to and used by the open-source community. Just because we encountered an exception does not mean that the application needs to be shut down. We can leave that decision to the developer who is implementing our library.
function getWeather(int $long, int $lat): array { try { // Send a request to a third-party weather API if(!$response->body()) { // Throw an exception but resist the urge to handle it. // Maybe the application can run in a deprecated stated, // e.g. fetching the last known weather data from a cache. throw new Exception("Failed to connect to the API."); } } // Organize and return the weather data }
Catch specific and custom exceptions
Always catch specific exceptions rather than using a generic catch-all block. This approach allows you to handle different types of errors appropriately and makes debugging easier. PDOException, RuntimeException, and InvalidArgumentException are some of the common exceptions we see in our applications.
class DivisionByZeroError extends Exception {} function calculatePayroll() { try { // Script runs here and can fail in multiple ways... } catch (DivisionByZeroError $e) { // Respond to bad inputs breaking the calculation... } catch (PDOException $e) { // Respond to a database issue... } catch (Exception $e) { // Respond to any other issues... } }
Use finally blocks for cleanup
The finally block is always executed and can be used to perform cleanup tasks, such as closing open files, unsetting variables, or closing a database connection. For this example, our method is to bake a cake. While many things can go wrong, we still need to clean up the kitchen when we are done.
protected function bake_cake() { try { // Do all the things to bake a cake... } catch (MixedUpBakingSodaAndBakingPowderError $e) { // We’ve all been there... } catch (OvenBrokenException $e) { // Call for repairs... } finally { // Always clean up at the end $this->cleanUp(); } }
Document throwables
Always include comments or documentation about your exception-handling strategy. Annotations allow IDEs and code tools to know what throwables may come their way.
/** * Writes a file to the specified destination and creates a file entity. * * @param string $data * A string containing the contents of the file. * @param string $destination * A string containing the destination URI. * * @return \Drupal\file\FileInterface * The file entity. * * @throws \Drupal\Core\File\Exception\FileException * Thrown when there is an error writing to the file system. * @throws \Drupal\Core\File\Exception\FileExistsException * Thrown when the destination exists. * @throws \Drupal\Core\File\Exception\InvalidStreamWrapperException * Thrown when the destination is an invalid stream wrapper. * @throws \Drupal\Core\Entity\EntityStorageException * Thrown when there is an error saving the file. */ public function writeData(string $data, string): FileInterface { // Write data functionality }
Antipatterns to avoid
Antipatterns are common practices or patterns in software development that may appear to be effective solutions, but often lead to more problems and inefficiencies in the long run. These bad practices can result in hard-to-debug code, silent failures, and overall instability in your applications.
Swallowing exceptions
Avoid catching exceptions without handling them. This practice, known as “swallowing exceptions,” can lead to silent failures and make debugging difficult. Something broke, but we never heard about it.
try { // Code that may raise an exception } catch (Exception $e) { // Don’t leave this empty just to hide problems }
Overusing catch-all blocks
Catching all exceptions with a generic catch block can mask errors and make it hard to identify the root cause of a problem. By catching all exceptions, you lose the granularity of handling specific types of exceptions differently. For example, a database error might require different handling than a network error, but a catch-all block treats them the same.
try { // Try running code with a focused set of responsibilities // this way we can have a predictable set of failures } catch (Exception $e) { // Don’t lump all problems into a single Exception type // as different issues will require different remediation, // see ‘Catch Specific and Custom Exceptions’ example above }
Relying on exceptions for flow control
Using exceptions to control the flow of your program, rather than handling expected conditions, can lead to convoluted and inefficient code. Exceptions should be reserved for truly exceptional situations. You do not need exception handling as a substitute to check if every variable is set or type is correct.
try { $value = intval($_GET['number']); } catch (Exception $e) { $value = 0; // Avoid using exceptions for flow control }
Don’t make exception handling an afterthought
The mechanics of exceptions are likely familiar to most readers of this article. It’s a proven pattern that has become integral to most programming languages. However, I hope this serves as a valuable reminder of how thoughtful error handling can have a lasting impact on the health of our applications. Catching exceptions and recovering from errors ensures the stability, reliability, and maintainability of our projects.
By avoiding antipatterns like overusing catch-all blocks and adhering to best practices, you can handle errors more effectively, provide meaningful feedback to users, and make it easier to debug and maintain your code.
Making the web a better place to teach, learn, and advocate starts here...
When you subscribe to our newsletter!