Test-Driven Development (TDD)

Summarized using AI

Using Monads for Elegant Error Handling

John Gallagher • November 08, 2021 • Denver, CO

In the presentation titled "Using Monads for Elegant Error Handling" at RubyConf 2021, John Gallagher discusses the challenges of managing exceptions in Ruby applications and how adopting a functional approach using monads can streamline error handling.

Gallagher highlights several key issues with the traditional method of handling exceptions in Ruby:
- Difficulty in understanding code: When an application fails, developers often find it hard to trace the source of the error due to scattered exception handling.
- Clumsy exception rescue: Normalizing exception rescue makes the code difficult to reason about, leading to potential bugs and maintenance issues.
- Coupling between methods and error handling: Developers may inadvertently confuse different failure modes due to returning nil or empty arrays, which can lead to faulty logic in the client code.

To illustrate these issues, he provides an example involving a customer address-editing process that requires multiple API calls. In the traditional approach:
- API failures lead to returning nil for a failed request, or an empty array for no results, making it unclear what went wrong.
- This can cause confusion in the controller that processes results, increasing cognitive load and reducing code clarity.

Gallagher introduces the concept of the Result Monad as a solution to these problems. He emphasizes that the Result Monad can encapsulate both success and failure states, providing a consistent interface for error handling. The benefits of using Result Monads include:
- Improved clarity and reasoning: With a clear distinction between success and failure encapsulated in distinct objects, developers can handle errors in a more structured way.
- Pipelines for composing operations: Gallagher explains how monads allow for cleaner, more readable pipelines that can handle series of operations without deeply nested if-else structures.

He demonstrates how to refactor existing code to utilize dry-monads, highlighting the following points:
- The new system allows chaining operations with fmap for successes and or for handling failures.
- The controller code is simplified by eliminating the need for multiple checks and allowing for straightforward error handling in a linear flow.
- Errors can be preserved for logging or further action, unlike the conventional method where exceptions might be lost.

In conclusion, Gallagher encourages developers to consider adopting monads as they can make error handling more elegant and manageable. However, he warns that such a transition involves further learning and could increase boilerplate code and dependencies. Ultimately, monads enhance readability, scalability, and maintainability of Ruby applications, making them a fitting choice for error handling in complex systems.

Using Monads for Elegant Error Handling
John Gallagher • November 08, 2021 • Denver, CO

Your app blows up in production. It's an outage and you're under pressure. You read the code. "How does this work? Why is this exception being caught here?" And so begins a long, stressful journey to understand how to fix your code.

Rescuing exceptions is normalised in Ruby, but it's a clumsy way of reacting to error conditions and causes your code to be difficult to reason about.

You'll refactor an existing app to use a functional style and see first hand how easy monads are to use and how they can make your code incredibly clean and expressive.

RubyConf 2021

00:00:11.360 Hello, RubyConf! I'm going to be talking about using monads for elegant error handling.
00:00:14.799 My name is John Gallagher, and I'm a lead developer at BiggerPockets and an automation consultant for my own business.
00:00:20.640 Before we start, I want to give you a little warning: many of these slides will contain extremely cheesy stock imagery for comedic purposes. You'll know because, ping, you'll get a little bit of cheese in the top right-hand corner. So let's get going!
00:00:27.519 This is the first photo that is pretty cheesy. This person here is obviously thinking, 'Who stole my finger puppet?' and this person is thinking, 'Did I leave the shower on this morning?' So imagine you're in a meeting like this where you're deciding what the next feature you want to build is. You decide, 'Okay, it's this.' You do some refinement of the card, and then you start coding like a good developer.
00:00:53.920 You do TDD, right? Because everybody loves TDD here; we're all Rubyists. And so off you go with your coding. You create the happy path, you handle an edge case, and at some point, if you're anything like me, you stop and think: 'Hang on, have I caught all the failure modes for this piece of code? Have I handled all exceptions? And more importantly, have I caught the right exceptions? What would the customer see if this piece of code went wrong? What should they see?'
00:01:36.560 Let me take a little example here of editing an address. Imagine you have customer's contact details stored in an external API, like Office 365, for example. Now, let's say you're at a stage in the process where you want to show a list of all addresses to select for their postcode. If you're in the U.S., you'd refer to it as a zip code. This drop-down might contain "1 Main St," "5 Main St," "3 Main St," and a whole bunch of other addresses.
00:02:04.719 So, the first thing you want to do is make an API call to get the postcode you've previously stored. However, every API call can succeed or fail, so what happens if it fails? You decide that you would just want to render an error to the customer, essentially giving up because there's nothing more you can do.
00:02:39.920 If that API call is successful, it will return a postcode, and from that postcode, you can then look up what addresses are at that postcode or, again, zip code if you're in the U.S. Now, of course, that is a second API call, and it can fail as well. If that succeeds, you just want to render a select on the screen with all the addresses for the customer.
00:03:01.760 If it fails, we want to render a manual address entry screen, so the customer can enter the state, zip code, and street. This way, they aren't prevented from moving on to the next step. How would we do that in a standard Ruby way using exceptions? Here's what it might look like.
00:03:39.760 You would have a class, maybe called FindContact, and it would have a standard method called call that passes in the ID it wants to look up. You would use something like Rails.configuration.api to call out to Faraday, where we make a GET request. Since this is a JSON API, it will return a body with some JSON. You fetch the contact key out and return that. If there's any error, we rescue Faraday error or maybe we rescue JSON parsing error.
00:04:35.679 My colleagues have seen situations where an API returns a 200 success, but the JSON is malformed because someone made a mistake somewhere. We want to catch both exceptions because they indicate something has gone wrong. Incidentally, Faraday error also includes things like timeouts.
00:05:14.760 Now, what happens if there's an error? We want to return nil. But why nil? Well, nil in this case is a special value that indicates to the client that something went wrong and the operation could not be completed.
00:05:58.919 The lookup for postcode is very similar: it still makes an API call, passes in JSON, and fetches the addresses key. If there's an error here, we decide to pass back an empty array of addresses. What would the controller code look like?
00:06:34.160 Before, it might look a little like this if you're using Rails. We would find the contact and return it. If that contact is nil, we'd render the error screen. Then we exit early, as the contact is anything. We continue to look at the postcode. If there are any addresses in that array of addresses that come back, we render the address select screen.
00:07:23.040 Otherwise, we render the manual address entry screen. However, there are a few problems with this approach. First, we're returning nil. I have a big issue with Ruby developers inserting nil into their programs. Occasionally, I've done the same myself, but there are reasons to avoid it.
00:07:44.160 The client also knows too much. The client, which in this case is the controller, knows what nil means. We have a situation where the client is tightly coupled with our logic. This leads us to our next concern: there's no consistency in what to return. Should it be nil, an empty string, or an empty array? This lack of convention increases cognitive load on developers.
00:08:44.400 The end result is that sometimes customers get software that feels broken because we haven't fully thought through these exception cases. If we do return nil, calling any method on it could lead to unexpected errors. This uncertainty creates a sense of doubt and decision fatigue.
00:09:32.000 So, what's the solution? Until recently, I didn't have a great answer, but I've discovered monads, specifically a result monad. Monads can be used for many purposes, but let's zero in on result monads.
00:10:01.920 The result monad is pretty straightforward: it's a class that represents success or failure with a consistent interface. You'll see that this means we can compose lots of steps together in a clean way. In case of success, this object wraps the value; in case of failure, it wraps the error.
00:10:41.679 Let's refactor the code we had before to use monads and see what happens. First, we need to understand how to create a success or a failure.
00:12:09.440 Using the dry monads gem, we can create a success by using the success class, unsurprisingly. You can do success.new if you prefer that, but dry monads offers a nice syntactic sugar that makes your code cleaner.
00:12:38.880 What about failure? Failure provides a similar interface. In this case, failure wraps the error, and you can pass in various objects, such as an exception, symbol, or open struct. However, calling value bang on a failure won't mean anything, and instead, it will throw an error.
00:13:24.160 The takeaway here is that if you're using success and failure and you want to unwrap the value, you should first check if it's a success. Once verified, you can then call the value bang to get the actual value.
00:14:23.360 Let's take the previous error-handling code and convert it to use success and failure. First, we include dry monads. We wrap what was coming back, converting it from a plain hash into a success that wraps that hash. Then we rescue any exceptional case and wrap it in a failure.
00:15:08.320 The same approach applies to looking up the postcode. We include dry monads, wrap the successful response, and similarly wrap any exceptions in a failure to pass back.
00:16:15.840 What would the controller look like? Initially, we used nil checks, now converted to failure checks. This ensures an early exit if there's a failure. If the operation succeeds, we can use value bang to extract the postcode as usual and render the address select screen.
00:17:09.360 So that’s it! That’s all I have. I've been John Gallagher, and thank you for your time.
00:18:03.120 But wait! You may now be thinking what the real benefit of this approach is. You’ve probably noticed that now there are many dot value bangs everywhere alongside failure checks. Does that really improve the code?
00:18:51.440 The answer is yes, and let me elaborate. We can now build a pipeline. This is where the true power of monads comes in. Using a pipeline allows for clearer handling of success and failure without losing readability. We want to take all the success values and pass them along in a clean and straightforward way, while any failures pass through without triggering all the error handling.
00:19:59.920 Fmap allows us to process those successes and pass the values through cleanly, while failures will be handled specifically without complicating the initial structure.
00:21:24.160 Now, let's compare the two bits of code side by side: on the left is the old style with exceptions, and on the right is the new approach with monads. While using the traditional flow may seem familiar, the pipeline structure brings increased readability, ease of use, and extends flexibility.
00:23:09.600 With this, you've unlocked a continuous approach to your logic that helps avoid confusion around error handling.
00:23:29.680 You've also got versatile options in terms of additional error handling down the line, which is much easier with this setup. So, there you go! While it might seem like another learning curve, this method offers a robust alternative.
00:24:38.880 Finally, a couple of practice questions: if we pass in a postcode that is not nil or blank, we get a success; if it's blank, it results in failure.
00:25:10.640 Thank you for your time, and if you're interested, I have an exercise available on GitHub where you can submit a PR, and I'll take a look.
00:26:00.000 Once again, I'm John Gallagher at BiggerPockets. You can find me at synaptic mishap on Twitter and LinkedIn or John Gallagher on GitHub. Hopefully, I'll see you on the other side, and thanks once again!
00:27:00.240 Enjoy the rest of the conference!
Explore all talks recorded at RubyConf 2021
+95