One of Rails 6’s new features is ActionableErrors, which allows you to add buttons to Rails’ default error page to perform certain actions. Out-of-the-box Rails 6 includes one for the common “pending migration” error, where you can hit a button on the page and run the migrations without having to jump back to a terminal.
Creating Custom ActionableErrors
To create your own “actionable error” is quite simple. You create an error class that includes ActiveSupport::ActionableError
, then you can use the action
method to create the button and its behaviour. Here’s a simple example from Rails itself:
class PendingMigrationError < MigrationError
include ActiveSupport::ActionableError
action “Run pending migrations” do
ActiveRecord::Tasks::DatabaseTasks.migrate
end
end
The string here (“Run pending migrations”) is what will be shown on the button. The block is what will be executed when the button is clicked. This is normal Rails code, so you have access to all your models and other objects.
When To Use Actionable Errors
ActionableErrors are great, but they require a custom error object. This means you probably only want to use them for errors that your code is raising. Further, you have to weigh the cost/benefit of maintaining an additional piece of code against the productivity gain of having a button there to fix an issue.
At work we recently added an ActionableError
to an internal gem. In our case, we support a form of multi-tenancy by selecting a Store
based on the requests’ domain (a very common way to handle multi-tenancy scoping). If we can’t find the store, we raise an exception.
This works but causes a minor headache every time a developer pulls down a new database dump from staging to run locally. We also don’t enforce how a developer sets up their machine so they could be using localhost
, or lvh.me
, or pow, or manually editing their hosts
file, etc, etc.
This made an ideal candidate for an ActionableError
— it is an error we are raising within our code, it has a simple and repeatable fix, and it is something that our developers encounter often.
Dynamic Actionable Errors
The ActionableError code in Rails is quite simple and I’d encourage anyone interested to read through it. One thing it does not have obvious support for is dynamic actions (at least at the time of writing).
In our Store
case above we really want to generate the buttons dynamically — we don’t know how many Store
objects are in the database, nor do we know which domain the request is coming from until runtime.
To get around this, we’re shadowing the _actions
class-level variable with a method that returns our dynamic list of actions:
def initialize(msg, domain)
# actions are class-level methods so we need to keep domain accessable
@@domain = domain || "nil"
super(msg)
end
# ActionableError expects actions to have been defined at the class-level, but
# we want to declare them dynamically, so we shadow the _actions variable and
# create them on-the-fly here
def self._actions
Store.all.map do |store|
["Change Store #{store.name} (#{store.url}) url to: #{@@domain}", -> { store.update!(url: @@domain) }]
end.to_h
end
You can see here we just iterate through all the Store
objects and create a hash mapping <text to show on button>
to a lambda that will be called when that button is clicked.
Conclusion
ActionableErrors are a great quality-of-life improvement for Rails developers. Going forward I’d love to see Rails provide an official way to define custom actions, but our work-around here solves that problem for now and seems unlikely to break in future Rails versions.
I wouldn’t try to force this onto a lot of errors, but for those times when you’ve said to yourself “Argh I always forget to <simple action>
after I’ve <common procedure>
”, ActionableErrors could save you that extra bit of time and sanity.