skip to content
Newvick's blog

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 data
  • Failure 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)
end
end
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?.

Result::Success
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
end
end

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.

Result::Failure
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
end
end

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

If you're interested in updates, you can subscribe below or via the RSS feed