Blog

Reducing Developer Friction With Rails 6's Actionable Errors

Placeholder Avatar
Raluca Pintilii
March 20, 2020

store-not-found-error

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:

ruby 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.

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:

ruby 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.