[RFC] Nicer error messages when some libraries aren't required

It’s RFC week! :grin:

If you write a code like this:

puts [1, 2, 3].to_json

You get this error:

Error: undefined method 'to_json' for Array(Int32)

Huh? But Object has a to_json method, I see it in the docs!

Well, the problem is that we define that method for all objects when you require "json", and given that the docs include all docs of the standard library, it’s not clear that this only works if you require "json".

So the first thing we should do is to actually document that this will only work if you require "json".

Another thing we could do is to define a to_json method on Object even when “json” is not required. Like this:

class Object
  def to_json
    {%
      raise <<-MSG
        undefined method `to_json` for #{@type}
 
        Did you maybe forget to `require "json"`?
        MSG
    %}
  end
end

Then with the original [1, 2, 3].to_json code the error you get is this now:

Error: undefined method `to_json` for Array(Int32)

Did you maybe forget to `require "json"`?

You can see that running here: Carcin

The only downside of this is that if for some reason you use another JSON library, not the standard one, that error might be confusing. But… I don’t think there’s another JSON library. The one in the standard library is pretty good! And we should also aim for the 99% of users who are going to use JSON.

This approach might sound a bit hacky or unsound, but I’m sure it will help people a lot. Pragmatism is what Crystal is all about.

A second thing we can do here is that once you do require "json", if you try to convert any object to JSON it won’t work:

require "json"

class Foo
end

Foo.new.to_json

The error is:

Error: no overload matches 'Foo#to_json' with type JSON::Builder

Overloads are:
 - Object#to_json(io : IO)
 - Object#to_json()

What? I did require "json"! And I can see there are some overloads… so what’s going on?

So here we could define this:

class Object
  def to_json(builder : JSON::Builder)
    {%
      raise <<-MSG
        no overload matches '#{@type}#to_json' with type JSON::Builder

        Maybe #{@type} doesn't `include JSON::Serializable`?
        MSG
    %}
  end
end

Then the error you get is this:

Error: no overload matches 'Foo#to_json' with type JSON::Builder

Maybe Foo doesn't `include JSON::Serializable`?

I think that’s a bit better.

An alternative to this is to not define to_json and to_json(IO) on every object, only on the ones that actually define to_json(JSON::Builder). I don’t know.

Then we could do something similar with YAML.

What do you think?

7 Likes

That’s one reason why I think using class methods on the JSON namespace, like JSON.serialize(object) is better than defining methods in Object that are added to all our Crystal objects, no matter what.

Even though with this RFC makes the compilation error clearer, before running the compiler it won’t be obvious from the API docs/IDE that this to_json method is in fact not functional (for some objects).

Better errors are always a good thing in my book!

3 Likes

Related discussions:

1 Like