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
Successobject with its data - if something went wrong, return a
Failurewith additional error information
Let’s work backwards. How do we want to use this Result monad? We want our API integration to return either:
Successwith the dataFailurewith 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)endThis 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 endendFinally, 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 endendAnd 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