Today I was working on upgrading the version of Dentaku gem we use for resolving user-provided, untrusted formula inputs.
I was able to observe an interesting evolution of how error classes are treated in the gem:
-
A naive implementation - there's just one custom error and most of the API relies on built-in errors like
RuntimeError
andArgumentError
. Needless to say, this is a poor showing, problems will be hard to debug. - Next step in the evolution - some more custom errors get defined for common failure spots. Debugging is improved.
-
Final form? A rich set of errors is defined, and, most importantly, all gem's errors inherit from
Dentaku::Error
base class, allowing simple catch-all rescues in using code:
def my_method
Dentaku(my_formula)
rescue Dentaku::Error # will handle Dentaku::ParseError, Dentaku::TokenizerError etc.
warn_user("Something is incorrect in the formula")
end
The takeaway
When working on a gem or even a component in a larger system, treat errors as 1st class API citizens, alongside inputs and outputs. Document them and apply good software architecture principles. Have a way to expose any raised errors in method docs, as well as having a glossary of all errors possible in the code.
Distilling Dentaku's approach we get this file structure:
dentaku/
lib/
dentaku/
exceptions.rb
dentaku.rb
And zooming in on what exactly goes on in exceptions.rb
:
module Dentaku
# The "abstract" toplevel parent error class for all errors
# raised by this gem, allows users to do
# `rescue Dentaku::Error` in their code for a catch-all solution.
# Also gives an opportunity to have all errors inside the
# gem have the same API, some metadata etc. depending on
# whether this is needed for the domain of the system.
class Error < StandardError; end
# A "concrete" error class for the gem.
# -- Description when this error is raised and possible solutions/changes needed to avoid it --
class ParseError < Error
end
end
Photo by Alexander Hipp on Unsplash