RailsConf 2024

Ruby on Fails: effective error handling with Rails conventions

Ruby on Fails: effective error handling with Rails conventions

by Talysson Oliveira Cassiano

The video titled "Ruby on Fails: effective error handling with Rails conventions" features Talysson Oliveira Cassiano discussing the various approaches to error handling in Ruby on Rails. In particular, Talysson emphasizes the importance of effective error management leveraging Rails conventions.

The presentation is divided into three main sections:

  • Introduction to Exceptions and Errors: Talysson establishes the difference between exceptions (which indicate recoverable situations) and errors (often indicating unrecoverable states). He explains that exceptions are used to manage scenarios like validation errors and payment issues, emphasizing the role of the raise and rescue methods in Ruby.

  • Common Anti-patterns: The speaker highlights several anti-patterns in error handling that developers should avoid, including:

    • Rescuing from Exception: Talysson warns against using the generic Exception class to rescue from errors, stating it may obscure actual problems and catch undesired errors, such as syntax errors.
    • Using Errors for Flow Control: He points out that error handling should not dictate code control flow. Instead, simple conditional statements should handle expected failures, preserving clarity and performance in the codebase.
  • Effective Error Handling Strategies: Talysson argues for intentional design around error handling, including:

    • Establishing clear and consistent strategies to handle errors.
    • Creating custom error classes that describe failure conditions extensively, allowing the caller to respond appropriately.
    • Logging critical errors and monitoring their context, particularly in critical systems like checkout processes or fraud detection.
    • Utilizing application-specific error conventions to create a lightweight architecture that adheres to Rails patterns.

Throughout the talk, Talysson illustrates his points with examples related to a checkout service, showing how to encapsulate functionality and manage errors without leaking unnecessary details into the calling code. He emphasizes the adoption of a centralized error reporting module, promoting a consistent way to handle and log errors across different parts of the application.

Talysson concludes with several takeaways, including:
- Avoid using errors for flow control, as they're meant to indicate failure.
- Design for errors proactively by considering all potential unhappy paths in advance.
- Centralize and structure error handling strategies while following conventions to maintain clarity and effectiveness.
By implementing these practices, Rails developers can achieve more robust and maintainable codebases, improving overall application resilience.

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!