Talks

Monadic Approach to Ruby Error Handling

#rubyconftw 2023

Monadic Approach to Ruby Error Handling

Error handling is an essential part of software development. No matter how well-written your code is, there will always be errors. The key is to handle them in a way that minimizes their impact.

In this talk, we will build a foundation of error handling concepts, categorizing errors and exceptions in Ruby. We'll look at practical code examples that demonstrate common issues when propagating errors through layers of code. Explore how Monadic Handling abstract away error handling details by combine Monads Result and Service Object in Ruby programming.

This talk will empower you understand how to use Monadic Handling Ruby code. You'll leave with a deeper appreciation of monads and concrete strategies for encapsulating errors elegantly in your Ruby applications. Join me to discover how monads enable you to minimize disruption and maximize control over error handling in Ruby.

RubyConf Taiwan 2023

00:00:28.960 Welcome everyone! Today, I will share about the monadic approach to Ruby.
00:00:46.079 How is everyone doing today? I am good! I've been traveling for weeks and I've fallen in love with Taiwan. The weather today is just perfect for the conference.
00:00:58.559 Welcome to the talk on the monadic approach to Ruby error handling. Exceptions in Ruby are well known to all Ruby developers as a unique data structure representing an error or unexpected condition in a program. It can contain information such as the error type, error message, and stack trace. Let's take a look at a code snippet that demonstrates how to handle a common error: division by zero.
00:01:34.880 In the snippet, we have a divide function that handles division by zero, which is a quite common error. If division by zero is attempted, an error is raised, and we print out the details to see what shows up on the console. Additionally, I raised an error if the result is unexpected.
00:02:08.080 Imagine we are creating a solution in Ruby. Today, I will showcase a web application for ticketing because it is a familiar domain. I will visualize a high-level design system as the backbone for our web application. It starts with a user who interacts with the frontend to buy a ticket. The user journey is facilitated by the backend server.
00:02:40.159 The ordering process begins with using the user ID and the ticket ID. The order is stored in a database, and we also integrate with a payment gateway for our solution.
00:03:05.519 Let's dive into the application. We want to build our blocks for the server using a straightforward architecture, where the controller interacts directly with the payment gateway, bypassing the view component. I came up with a nice implementation, though it may look a bit overwhelming at first, but don't worry—I will break it down.
00:03:30.720 Initially, when a user buys a ticket, we need two pieces of data: the user ID and the ticket ID. Then we search for the corresponding user. If we find the user, we create an order with that user and check the user's balance against the order price. If the balance is sufficient, we continue with the transaction.
00:04:05.200 The controller interacts with our payment gateway to initiate the payment. This is where we integrate the external service to process the payment. Upon successful submission, we confirm the purchase by rendering a successful response to the user.
00:04:43.560 Next, we handle three main value points: user balance, record retrieval, and payment submission. If the user's balance falls short, we render an 'Insufficient Balance' message to the user. We also handle exceptions in a traditional way when dealing with errors.
00:05:09.800 If the payment submission fails, we again catch the exception. Each step must inform the user clearly with an error message.
00:05:57.400 Now, do you have any comments? This approach is not very extensible or reusable. Can we make it better?
00:06:02.440 Before we continue with the talk, let me introduce myself. My name is Du Gia Huy. I am a software engineer with five years of experience working on high-traffic web applications. I currently work in Singapore and have presented at RubyConf Thailand. Today, I'm excited to be here at RubyConf Taiwan.
00:06:39.919 At my company, we build systems that deliver content to millions of users around the world on a daily basis. Our platform is a leading service for Asian entertainment and culture, where millions discover and consume their favorite shows and movies.
00:07:09.760 So let's discuss what programming is about. It involves crafting solutions to a set of instructions and giving them to the computer to solve problems. As software engineers, our primary job is to solve problems.
00:07:52.560 Everyone is likely familiar with the mathematical concept of functions, where f(x) = y. Here, f represents the solution, x is the data parameter needed to execute the logic, and y is the expected output when executed correctly. However, if we execute the function with specific input and the output differs from our expectation, this results in a software error.
00:08:40.159 We can categorize errors in software into two main types: expected and unexpected errors. Expected errors are part of program execution. As software engineers, it is our job to anticipate these expected errors and prepare for normal operation. For example, we might expect errors due to business logic constraints or external dependencies.
00:09:36.760 On the other hand, unexpected errors occur outside our control, such as database issues or memory failures. However, we can manage these unexpected errors through diligent monitoring and improving our software.
00:10:02.000 In this talk, we will focus on how to handle expected errors effectively. By mastering expected errors first, we can better tackle unexpected errors later in our daily software engineering.
00:10:56.880 Our goal is to create a robust system that minimizes disruption and prevents incidents. We want to ensure that when a user tries to buy a component, the implementation is improved. I introduced the concept of a business logic layer that encapsulates our core functions and decision-making.
00:11:36.919 In contrast to passing requests through various layers, we have this business logic layer perform necessary functions. This separation of business rules makes our system more maintainable and adaptable to future changes.
00:12:20.560 Furthermore, this approach simplifies our architecture. We can integrate with other front-end systems or messaging platforms without starting from the controller.
00:12:39.839 Now, let's discuss refactoring and improving our initial implementation. At first, we handled multiple responsibilities within the controller. This led to a messy design, so I decided to modularize it using the business logic layer.
00:13:08.440 I wrapped the order creation into a dedicated service. The payment mission was also separated for clarity. This change has ensured our controller remains clean, making the code much more maintainable.
00:13:47.280 After creating an order, we have wrapped it into create order and checkout services, so the flow remains essentially the same while improving clarity and reducing our technical debt.
00:14:34.920 Now let’s move on to exception handling. In the initial implementation, the controller handled errors directly, while in the new design, we propagate errors back to the user clearly and concisely.
00:15:23.640 The difference here is that now the maintainer can more easily see and understand the error scenarios compared to the traditional way.
00:15:43.680 As we continue, we should maintain our focus on error handling and how it can further be improved.
00:16:02.960 Currently, our error-handling remains limited. Can we improve this further? Now, when we enhance our error handling, it should not just be clear and readable, but also flexible, reusable, and encapsulated across different parts of our services.
00:16:30.960 This brings us to the next iterative version of our refactoring, which aims to encapsulate error handling inside each service rather than in the controller.
00:17:20.800 For each service, we will have separate error handling that keeps the controller logic streamlined while simplifying error management.
00:18:00.800 This facilitates a clearer separation of concerns. While we now focus on the outputs and can process errors better, we're still facing the challenge of keeping the controller agnostic about specific service-level errors.
00:18:50.200 By utilizing monads, we can standardize our error handling across the software, ensuring consistency and clarity. Monads encapsulate the results of operations and manage them predictably.
00:19:57.840 They allow us to handle the complexity of error management smoothly, focusing on binary outcomes (success or failure) and creating a clean, maintainable error handling process.
00:20:31.280 When implementing monadic handling, the result consists of either a success with a value or an error. This binary approach simplifies how we handle function results, allowing us to maintain clarity and consistency across our software.
00:21:46.320 Now, let’s explore how we can effectively represent the monadic result in our Ruby programs.
00:22:20.880 We can use constructs like OpenStruct to define a structure that holds both the success value and any accompanying error details. This way, we can operate on error handling and function results clearly and consistently.
00:23:17.120 Another crucial component is the Service Object, which is designed to execute a single action in our business logic while centralizing our set of operations such as validations and calculations.
00:23:51.640 The Service Object approach leads to better-defined APIs for all parts of our application.
00:24:29.200 In our proposed monadic approach, we leverage the Service Object and monadic results to create a resilient system that gracefully handles both success and error states of operations.
00:25:16.560 Through this strategy, we ensure error handling is cleaner, more maintainable, and allows separate handling of different parts of our software.
00:26:06.560 In actual implementation, we can create a base class for our service that encapsulates the monadic results and operates on our business logic, enhancing the clarity of our controllers.
00:27:09.760 As we implement this in our application workflow, we see that error handling is done more clearly, leading to better maintainability and a smoother overall flow.
00:27:56.680 Additionally, we have other libraries like Dry Monads that offer robust tools for implementing monadic strategies in Ruby.
00:28:51.040 In the context of error handling, we constantly ensure the validity of inputs to avoid undesired function outcomes.
00:29:34.560 By doing this, we reduce the chances of unexpected errors and improve the robustness of our applications. It is crucial to distinguish between validity and execution errors to minimize disruptions in our software development.
00:30:29.680 Our encapsulation strategy enhances our flow management. Utilizing traditional exception handling is typically more reactive, while a structured approach allows us to handle input issues effectively before they become runtime exceptions.
00:31:57.920 In our talks, we emphasize that both error handling techniques—traditional and monadic—are complementary rather than mutually exclusive.
00:32:58.720 As we raise exceptions, it's essential to consider whether the error is due to an expected failure that should be handled proactively, further developing our software's robustness.
00:33:45.440 This talk aimed to provide a new strategy for error handling by understanding different error types in software applications and creating robust business logic layers to encapsulate errors effectively.
00:34:37.447 Thank you for your attention! Do you have any questions?
00:35:04.680 I appreciate your questions! Yes, we often face errors while calling services and need to handle the errors effectively to maintain a seamless user experience.
00:35:46.800 Understanding the distinction between expected and unexpected errors is crucial in implementation. By utilizing a standard and clear approach, we can differentiate these errors and address them appropriately.
00:36:37.720 As software engineers, we should strive for clarity in our error handling to improve our systems. The goal is not just to fix errors as they arise but to build solutions that minimize these errors from the start.
00:37:52.400 Thank you all for your insightful questions. Let's continue to improve our error handling strategies as we work together in teams. Remember, consistency is key, and clear communication among team members about error handling can enhance collaboration and efficiency.
00:39:43.680 So now, let's take a break until 1:30 PM when we'll continue with the afternoon sessions. Thank you all!