When all you need is Success and Failure (Result Monad)
When building a service that handles many different API integrations, it’s helpful to standardize the response. The Result
monad is useful for expressing the response as either Success
(everything went well), or Failure
(something has gone wrong).
Ruby has a library called dry-rb that has a good implementation of the Result
monad. Not just that, it’s packed with monad goodies. But it has more complexity than I needed. I don’t need to chain a series of computations.
So let’s create a simple version of the Result
monad.
We just have 2 goals:
- if the response went well, return a
Success
object with its data - if something went wrong, return a
Failure
with additional error information
Let’s work backwards. How do we want to use this Result
monad? We want our API integration to return either:
Success
with the dataFailure
with the error
def fetch_user(id) response = api.get("/users#{id}")
if response.ok? Result::Success(response.data.json, response) else Result::Failure(get_error(response), response) endend
result = fetch_user(id)if result.success? handle_success(result.data)else handle_failure(result.error)end
This looks simple to use. We just need to check result.success?
and then we can retrieve the info.
Next let’s implement Result::Success
. We want to be able to access data
and response
(hence, the attr_reader
), and functions to check for success?
or failure?
.
module Result class Success attr_reader :data, :response
def initialize(data, response = nil) @data = data @response = response freeze end
def success? true end
def failure? false end endend
Finally, we will implement Result::Failure
. It looks almost the same, except we don’t have data
, but we do have error
. And the booleans for success?
and failure?
are the opposite of Success
.
There are 3 convenience functions that let us easily check what type of error it is and handle it appropriately.
module Result class Failure attr_reader :error, :response
def initialize(error, response = nil) @error = error @response = response freeze end
def success? false end
def failure? true end
# Convenience functions def client_error? error.is_a?(ClientError) end
def server_error? error.is_a?(ServerError) end
def status response&.status end endend
And that’s our Result
monad!
We didn’t need to import a complex library to get just the functionality we needed. Of course, we can add more functionality later on.
💬 Have thoughts on this post? Send me an email or use this form