00:00:10.519
Right now, we are going to be doing a presentation on Ruby on Fails: effective error handling with Rails conventions. The speaker is Talysson Oliveira Cassiano, a web developer and software architect.
00:00:16.480
He is a true believer that we can always do better.
00:00:35.960
Can you all hear me? Okay, great! So, as just mentioned, we're going to talk about Ruby on Fails and effective error handling with Rails conventions.
00:00:42.200
A quick introduction: my name is Talysson, and I'm a software architect and technical development manager at a Brazilian company called Code M4U. If you don't know us, we are a software boutique focused on Ruby on Rails and JavaScript.
00:00:52.520
If you need help with those technologies, just reach out to us. Now, today's talk is going to be divided into three parts: first, I'll discuss exceptions and errors; then we'll go over some anti-patterns; and finally, I'll explain how we can achieve effective error handling.
00:01:06.280
Let's start with exceptions and errors. In programming, exceptions serve as a mechanism to express 'unhappy paths'—situations where we cannot proceed normally. We use exceptions to handle scenarios from which we are able to recover. We call these scenarios 'error scenarios.' This lays out the main difference between an exception and an error.
00:01:22.840
In this talk, we will focus on some recoverable errors, such as validation errors, authentication errors, payment errors, and so on. In Ruby, exceptions facilitate communication between the raise method and the rescue function. The rescue part of the code is where we handle exceptions.
00:01:45.880
The base class for all exceptions is the Exception class, and recoverable errors will be descendants of the StandardError class. If you look at the hierarchy of Ruby error classes—which is quite extensive—you can see that if you rescue without specifying the error class, you are effectively rescuing from the StandardError class.
00:02:03.880
Additionally, if you raise without specifying what you are raising, it will default to a RuntimeError. This brings us to discuss some common error-handling anti-patterns. The first one is rescuing from exceptions just to be safe. You might think, 'If every error extends from Exception, then rescuing from Exception will cover all my bases.' However, using Exception is too generic.
00:02:21.280
Error handling should not be used for inherently invalid states or cases where recovery isn’t possible. If you rescue from an error without a solid plan, you might not be addressing the real problem. This leads me to advise against rescuing from Exception without a very good reason.
00:02:35.920
Going back to the hierarchy of errors, if you rescue from the base class—Exception—it will also rescue from many other exceptions, including syntax errors, which is often not desirable. For example, if your file contains a syntax error and you require that file inside a begin-rescue block that rescues from Exception, you'll handle the syntax error improperly, which leads to undesirable behavior.
00:02:58.800
This highlights why we should avoid rescuing from Exception just to be safe. Another anti-pattern is using errors for flow control. In the same vein, I’ve provided an example of a CardItem controller with some error handling. Observe how the raise function is used to control flow in locations where a simple conditional would suffice, ultimately complicating the code and making it less efficient.
00:03:23.160
When a method is supposed to handle conditions that could fail, it should not resort to raising exceptions for flow control but rather use ordinary conditional branches. Using errors as flow control results in code that is challenging to read and understand. Raising errors incurs overhead, and we should minimize their usage for performance reasons.
00:03:46.360
Now, let's return to the main content of the talk, which is effective error handling. To achieve effective error handling, we must first ask ourselves what our goals are. Our objectives include establishing a consistent and pragmatic error handling strategy, making error scenarios explicit, providing sufficient context for the caller to make decisions, and adhering to conventions within Rails.
00:04:18.120
The first step to achieving these goals is to be intentional in our programming. This means we shouldn't blindly guess errors as we write code; instead, we should discover potential errors beforehand. Before writing any code, it's critical to design your implementation while considering all possible unhappy paths and determining how you will handle them.
00:04:56.080
Remember, errors serve to communicate with the calling site rather than with the internal implementation. The calling site must have enough information and capability to handle errors properly. Additionally, you should only handle what you can recover from; if you encounter a situation that presents an irrecoverable error, it's better to let it crash rather than obscure the issue.
00:05:31.200
I have an example of a checkout service divided into several components: creating a card, processing orders, detecting fraud, and processing payments. If we closely analyze this code and explore potential unhappy paths—like products being out of stock, memory issues, or fraud detection failures—we should focus on scenarios we can manage within our operational business context.
00:06:01.240
Next, how do we effectively express error scenarios within our code? One practical approach is to create specific error classes that communicate what went wrong clearly. This becomes particularly important for methods that can fail due to multiple reasons. It’s essential for the caller to understand exactly why a failure occurred.
00:06:35.240
In Ruby, when creating a custom error class, it should inherit from StandardError. You can enhance this custom error class with additional methods and attributes to clarify what happened. For example, when an item is not available, we can create an 'ItemNotAvailableError' that contains information such as the item ID.
00:06:56.760
When we raise this error within the context of our checkout service, we can add explicit rescue clauses that inform the caller about the issue. This allows the controller to respond appropriately, perhaps by sending an informative HTTP status code or result back to the client.
00:07:25.520
It's important to remember that errors must be abstracted in the same way we handle logic. If we encapsulate functionality within a service, we need to ensure that errors from external services do not leak through. Should another service cause an error (e.g., an ActiveRecord::RecordInvalid), we should design our checkout service to handle that transparently for whoever is calling it.
00:07:52.840
For instance, if we have an invalid order scenario, instead of exposing ActiveRecord errors, we should raise an 'OrderInvalidError' within our checkout service. The controller calling the checkout service can safely handle this custom error without needing to directly interact with ActiveRecord specifics, ensuring no unnecessary information leaks.
00:08:24.600
We can create specific error classes for other scenarios as well, such as fraud detection scenarios. When fraud is detected during the checkout process, the service can raise a 'FraudDetectedError,' which can be rescued in the controller to handle a user-facing response appropriately.
00:08:58.560
It's important to log critical errors, especially for significant events such as fraud detection. You should also remember that when you raise errors within rescue blocks, Ruby will automatically associate these errors with the original context of the event through the error's cause method.
00:09:28.280
When designing error handling strategies, avoid hardcoding responses or centralizing error logic in a way that could lead to inconsistencies. You need to ensure you have a centralized error reporter, which can be established through Rails initialization. This reporter should have methods for accepting error messages and metadata.
00:10:08.200
Now, we can create a module for error handling that provides a uniform way to report errors without needing to pass specific details in each instance. By subscribing our error reporter to Rails' error handling mechanism, we can leverage built-in logging and reporting features that Rails provides.
00:10:31.840
Using the report error method in the module, we can efficiently log errors from various parts of our application (controllers, jobs, etc.) while maintaining a high level of contextual integrity. Ensuring our error classes are clear and descriptive also allows other developers to understand the logic easily.
00:11:07.840
Now, let’s revisit the use of conventions. Rails is known for its conventions, reducing the overhead of decision-making. However, Rails lacks consistent conventions for error management, aside from ActiveRecord. By establishing a solid framework of error conventions (like custom error types), we further enhance the robustness of our error management.
00:11:33.000
To create an effective convention, an ApplicationError can be established as a superclass for all custom errors. With this, errors that inherit from ApplicationError can be designed to include JSON representations tailored for API usage, such as status codes or detailed messages.
00:12:04.440
If you need more specific error details, you can include a 'details' attribute, which can provide additional context to help consumers of the API understand what occurred, such as validation error details.
00:12:28.700
However, while you could just add generic catches for the ApplicationError, it’s a better practice to derive specific error classes that provide contextual insights. The specificity improves clarity, and it allows for better error handling in various application scenarios.
00:13:01.200
As we explore our conventions and strategies, don't overlook application flow strategies like result objects and monads. They can be beneficial when correctly contained within your application’s context, rather than being a pervasive pattern throughout the codebase.
00:13:34.800
A common situation arises when methods interacting with standard Ruby objects need to transition into result object juxtapositions. Containing these approaches within layers that interface with external resources permits your other code to remain clean and maintainable.
00:14:06.720
Before wrapping up the talk, I’d like to summarize some takeaways from what we've discussed. First, refrain from using errors for flow control. Errors should communicate that something has gone wrong rather than direct code flow inappropriately.
00:14:38.720
Be intentional and explicit in your code; proactively design for errors rather than reactively discovering them. Centralize error handling in a structured manner, and develop and follow project conventions. Finally, keep strategies that impact the entire application contained within designated areas of the code to prevent widespread complications.
00:15:05.960
Thank you for your attention and participation!