In Progress
Unit 1, Lesson 1
In Progress

Error Value

Video transcript & code

In the last episode, we started with a function that raised an exception when it received bad input. We talked about various options for what the function could return to indicate a failure, rather than raising an exception.

Here's that function again. We settled on returning the the empty string on failure. We talked about how, in this particular context, the empty string is benign, meaning that it is unlikely to cause further problems down the line. And about how it is also readily identifiable meaning that methods needing to treat failure cases specially would have no trouble testing for it.

require "addressable/uri"

def custom_thumbnail_url(url, attributes)
  uri                     = Addressable::URI.parse(url)
  uri_params              = uri.query_values or return ""
  resize_param            = uri_params["image_crop_resized"]
  orig_width, orig_height = resize_param.split('x').map(&:to_i)
  width                   = attributes.fetch(:width) { orig_width }
  height                  = attributes.fetch(:height) { orig_height }
  uri.query_values        = {"image_crop_resized" => "#{width}x#{height}"}
  uri.to_s
end

As an example, we looked at this view helper method, which checks for both nil values and empty strings at the same time, and substitutes a default URL when a failure-flag is detected.

def episode_thumbnail_path(episode, root_path=settings.root)
  if !episode.video_thumbnail_url.to_s.empty?
    episode.video_thumbnail_url
  else
    "/images/video-thumbnails/default.png"
  end
end
require "./method"

result = custom_thumbnail_url("http://example.org/default.gif",
                              width: 640,
                              height: 480)
result                          # => ""

This solution is adequate for many cases. But as we noted in the previous episode, one area that it falls short is that it throws away all information about where and why an error occurred. The empty string says nothing about what went wrong and why; in fact, looking at just the empty string, we can't even be completely certain that something went wrong at all.

Assume that we already have a system which has numerous dependencies on the fact that this method will always return a (possibly empty) string. How can we add information to the output, while retaining backwards compatibility?

One way to do this is to model the error output as its own kind of type. Of course, whatever this method returns has to behave like a string, and the easiest way to ensure that is the case is to simply inherit from String.

class BrokenThumbnailUrl < String
end

Let's modify the function to return one of these BrokenThumbnailUrl values on error.

require "addressable/uri"
require "./error_type"

def custom_thumbnail_url(url, attributes)
  uri                     = Addressable::URI.parse(url)
  uri_params              = uri.query_values or return BrokenThumbnailUrl.new
  resize_param            = uri_params["image_crop_resized"]
  orig_width, orig_height = resize_param.split('x').map(&:to_i)
  width                   = attributes.fetch(:width) { orig_width }
  height                  = attributes.fetch(:height) { orig_height }
  uri.query_values        = {"image_crop_resized" => "#{width}x#{height}"}
  uri.to_s
end

This alone is enough to give us a little extra functionality. The error return value is still an empty string in every sense, and will work for any existing code that is looking for the empty string as a flag. But we can also use the new specialized type to check for specific problems.

require "./method2"

result = custom_thumbnail_url("http://example.org/default.gif",
                              width: 640,
                              height: 480)
result                          # => ""

if result.empty?
  puts "There was a problem"
end

case result
when BrokenThumbnailUrl
  puts "The thumbnail URL was broken"
else
  puts "Everything is fine"
end

# >> There was a problem
# >> The thumbnail URL was broken

Of course, it might be nice from a debugging standpoint to be able to find out what the problem input was. We can do that by adding a new attribute to this error value.

class BrokenThumbnailUrl < String
  attr_reader :original

  def initialize(original)
    @original = original
    super()
  end
end

Then we have to modify the code that uses it to pass in the original, problematic string.

require "addressable/uri"
require "./error_type2"

def custom_thumbnail_url(url, attributes)
  uri                     = Addressable::URI.parse(url)
  uri_params              = uri.query_values or return BrokenThumbnailUrl.new(url)
  resize_param            = uri_params["image_crop_resized"]
  orig_width, orig_height = resize_param.split('x').map(&:to_i)
  width                   = attributes.fetch(:width) { orig_width }
  height                  = attributes.fetch(:height) { orig_height }
  uri.query_values        = {"image_crop_resized" => "#{width}x#{height}"}
  uri.to_s
end

Now when we get an error return value, we can ask it for the original string. The object's intrinsic value as a string, though, is still empty.

require "./method3"

result = custom_thumbnail_url("http://example.org/default.gif",
                              width: 640,
                              height: 480)
result.original                 # => "http://example.org/default.gif"
result                          # => ""

Actually, in a way this error object is still a bit too string-like. If we are debugging and we take a look at the value of it, all we see is empty string. There's nothing to tell us that it's really a specialized subclass of string unless we think to ask.

We can remedy this by overriding and customizing the error type's #inspect method.

class BrokenThumbnailUrl < String
  attr_reader :original

  def initialize(original)
    @original = original
    super()
  end

  def inspect
    "BrokenThumbnailUrl(#{original.inspect})"
  end
end

Now when we inspect the object, it is very clear that it is not an ordinary string. However, when we're not inspecting it, it still behaves like an ordinary empty string.

require "./method3"
require "./error_type3"

result = custom_thumbnail_url("http://example.org/default.gif",
                              width: 640,
                              height: 480)
result
# => BrokenThumbnailUrl("http://example.org/default.gif")
puts result

# >>

It seems likely that once we have an error value, any attempts to modify that value are probably indicative of a bug. So one last addition we might make to this error value class is to have it freeze itself on initialization. That way it will fail loudly if we try to perform any fancy munging on an error value.

class BrokenThumbnailUrl < String
  attr_reader :original

  def initialize(original)
    @original = original
    super()
    freeze
  end

  def inspect
    "BrokenThumbnailUrl(#{original.inspect})"
  end
end

result = BrokenThumbnailUrl.new("http://example.org")
result << "?width=120"

# ~> RuntimeError
# ~> can't modify frozen BrokenThumbnailUrl
# ~>
# ~> xmptmp-in152569S1.rb:16:in `<<'
# ~> xmptmp-in152569S1.rb:16:in `<main>'

As I mentioned before, this isn't necessarily a technique we want to use everywhere. And it's almost certainly something that we would add later on, when we discover that we need more information down the line than a simple benign value can communicate. For instance, we might discover that we need to differentiate between different kinds of failure, something that having distinct error values makes very easy. Or, we may just want to give ourselves a better window into the history of a problematic value when we dive in to debug a problem.

In any event, it's good to remember that just because a return value is a String, doesn't mean it has to be just a String. Happy hacking!

Responses