Guide to Exceptions in Ruby
If you're experiencing unexpected behavior in your Ruby code, similar to what I've faced, and you're struggling to pinpoint the exact error, try looking into exceptions.
What's an exception?
Exceptions in Ruby, or any programming language, are crucial for handling errors gracefully. Here are some scenarios where you can appreciate their importance, especially when using an application.
Online Shopping Cart Error Handling: Imagine shopping online and adding items to your cart. Suppose there's an issue with one of the items (like it's out of stock) instead of the entire application crashing or freezing. In that case, an exception can be used to display a friendly message like "Sorry, this item is currently unavailable" and allow you to continue shopping. This prevents a complete disruption of your shopping experience.
Form Submission with Validation Errors: When you fill out a form on a website (like a sign-up form), and you forget to fill in a required field or enter invalid data, exceptions can be used to catch these errors and show you specific feedback like "Please enter a valid email address" or "Your password needs to be at least 8 characters." This helps in guiding you to correct the errors without losing the data you've already entered.
File Upload Errors: If you're uploading a file to a website, and there's an issue (like the file is too large or in an unsupported format), Ruby exceptions can be employed to catch these issues and inform you with a message like "File size exceeds the limit" or "Unsupported file format." This prevents the application from crashing and provides clear guidance on how to proceed.
Payment Processing Failures: If you're uploading a file to a website, and there's an issue (like the file is too large or in an unsupported format), Ruby exceptions can be employed to catch these issues and inform you with a message like "File size exceeds the limit" or "Unsupported file format." This prevents the application from crashing and provides clear guidance on how to proceed.
Database Connection Issues: If an application fails to connect to its database, instead of showing a cryptic error or a blank page, Ruby exceptions can catch this and show a more user-friendly message like "We're experiencing technical difficulties, please try again later." This informs you that the issue is on the server side and not a problem with your actions.
API Limit Reached: If an application uses external services (like a weather API) and exceeds its usage limit, Ruby exceptions can gracefully handle this by showing a message like "Weather data is currently unavailable, please check back later." This helps in maintaining a good user experience despite external limitations.
In each of these scenarios, exceptions in Ruby are used to prevent the application from crashing or behaving unpredictably and, instead, provide helpful feedback to the user, guiding them on how to proceed or informing them about the nature of the problem. This enhances the overall user experience and trust in the application.
Exceptions in Ruby
If you've ever made a typo in your code that caused your program to crash with a message like SyntaxError or NoMethodError, then you've seen exceptions action.
You should see the NoMethodError if you run this in your IRB.
Rescuing Exceptions
Let's say you have a method pizza_slicer that divides the slices equally according to the number of people.
Assuming that your user inputted 0 number of people, the program will raise an exception called ZeroDivisionError.
The process is called "rescuing," "handling," or "catching" an exception. They all mean the same thing.
Here's how you catch an exception in Ruby:
Here's the result:
Rather than terminating, Ruby executes the code within the rescue block, displaying a message.
Exception Objects
Objects representing exceptions in Ruby are standard Ruby objects. They contain data about the specifics of the exception that was just handled.
To get the exception object, we will need to modify our Ruby code to this:
Here's the result:
Custom Exceptions
Ruby's built-in exception does not cover every possible use case. Creating a custom exception is advisable if you are building a system and encounter a scenario where none of Ruby's standard exceptions fit.
What are these scenarios where you need to create custom exceptions?
1. Custom Login Failures: In an application with a login system, instead of just displaying a generic error message for all login failures, developers can raise custom exceptions for specific issues, like "IncorrectPasswordException" or "AccountLockedException." This allows the application to give more informative feedback to the user, like "Your account has been locked due to multiple failed login attempts." or email the user that there have been multiple attempts to log in to his account.
2. E-commerce Stock Management: Imagine you're shopping online and you try to purchase an item that is almost out of stock. Developers can raise an "AlmostOutOfStockException," which triggers a response to inform stock managers that an item is almost out of stock.
3. Age Restriction on Content: For applications with age-restricted content, developers can implement an "AgeRestrictionException" that gets raised when a user below the required age tries to access such content. This ensures the user is clearly informed about the age restrictions in place.
Using our pizza_slicer method error, here's an example of a custom exception.
Explanations:
ZeroPeopleError is a new exception class that inherits from StandardError. It overrides the message method to provide a custom error message.
Inside pizza_slicer, the raise keyword triggers the ZeroPeopleError manually if the number_of_people is 0.
The rescue block is modified to catch ZeroPeopleError. It prints the custom message.
Custom exceptions allow more precise control over the exception-handling process and make your code more readable and maintainable.
Class Hierarchy of Ruby Exceptions
Why does this matter? It matters because rescuing errors of a specific class also rescues errors of its child classes.
When you handle StandardError using rescue, you're also handling all of its subclasses, such as ArgumentError, IOError, and more, as shown in the chart. However, rescuing Exception means you're catching every possible exception, which is generally not recommended and can be problematic.
Rescuing Exceptions in Ruby
The Bad: Rescuing All Exceptions
The code above will rescue exception. The problem with that is if we have some typo like this:
The result:
The SyntaxtError exception will be caught, and your program will always return "You inputted 0 number of people".
Rescuing all errors is code smell, why?
Catches Too Broad a Range of Errors: Rescuing Exception will catch every possible exception, including those that are critical to the internal workings of Ruby itself. This includes system errors like SignalException (e.g., Interrupt), ScriptError (e.g., LoadError, SyntaxError), and even SystemExit. Many of these exceptions are used by the Ruby runtime to manage fundamental aspects of the execution environment and are not meant to be caught by application-level code.
Can Mask Serious Problems: By rescuing all exceptions, you may inadvertently hide serious problems in your code that should be addressed or at least logged. For example, a NoMemoryError indicates a serious issue that should not be quietly rescued and ignored, as it could lead to further instability in your application.
Makes Debugging Harder: When you rescue all exceptions, it becomes much harder to track down bugs because your code is effectively saying, "No matter what goes wrong, just continue." This can lead to situations where errors occur but go unnoticed, making it difficult to understand why something later in the execution flow isn't behaving as expected.
Potentially Disrupts Program Flow: Some exceptions, like SystemExit, are used to control the flow of the program (such as when it's time to exit). Catching and handling these can prevent the program from terminating as intended, leading to unexpected behavior.
Violates the Principle of Least Surprise: In programming, the Principle of Least Surprise means that code should behave in a way that's least surprising to someone who understands its context. Rescuing all exceptions can lead to surprising and unintuitive program behavior, which is generally considered poor design.
Best: Rescuing Specific Errors
Rescuing all errors is a lazy way to figure out which specific exceptions are need to be rescued. The best way to to do it is take time to figure out which exceptions you should rescue in your scenario.
For example:
You can also rescue multiple kinds of exceptions.