Localized error feedback in rails
by ewout
Sometimes, something goes wrong in an application. Showing a general error message to the user, even if it is really pretty, is like showing your middle finger. The general goal is to avoid errors, and when they do occur tell the user what is going on and give him a way to resolve the problem. A classic example is validation errors. Feedback for this class of errors is straightforward: mark the fields that have problems and explain what is wrong. Another example is actions which require the user to have certain permissions. Permission errors can generally be avoided by not including actions in the view for which the user does not have permission.
However, there are errors that cannot be avoided or handled by the user. Sticking with the permissions example: suppose a user has permission to perform action A. The page is loaded and shows a link to action A. While the user is filling out some data on the page, an administrator revokes the user’s permission to perform action A. When the user presses the button for the action now, the controller will raise an error. The only sane feedback for this error is informing the user that he no longer has permission to perform the action.
This post is about the kind of errors that cannot be handled. Speaking HTTP, it’s about error 422.
The goal
- Define exceptions that can be propagated to the user
- Explain each of these exceptions in every language the application supports
- Render the errors in a generic, DRY way, regardless of the request type (html or ajax)
There is a minimal example project on github that implements the idea.
Define the exception
DRY error handling means not having to rescue every possible exception in every action. Rails provides a controller class method rescue_from that avoids that repetition.
rescue_from(LockedError) { render_error :locked }
Exceptions that occur in multiple controllers can be defined in ApplicationController.
Explain the exception
In the locale files, the error can be explained in detail.
en: custom_errors: locked: title: Locked message: Could not complete the operation because the item is locked.
Render the exception
A single method becomes responsible for rendering errors to the user. For html requests, a template is rendered with local variables title and message. For ajax requests, the error message is returned in json format. When the site is also accessible as a web service, this method may need to return errors in different formats based on the requested mime type.
def render_error(scope, status=422) error = { :title => t(:title, :scope => [:custom_errors, *scope]), :message => t(:message, :scope => [:custom_errors, *scope]) } if request.format.html? render :template => 'application/error', :locals => error, :status => status else render :json => {:error => error}, :status => status end end
Growl and ajax errors
Growl is a javascript library that displays notices and error messages way fancier then alert().
Growl.Bezel({ title: 'Locked', text: 'Could not complete the operation because the item is locked', image: '/images/growl_warning.png'})
In this example, growl will be responsible for rendering errors that occur during an ajax request. It can be swapped out with any other notification method, the important thing here is there is one central piece of code on the client side responsible for showing errors to the user as they occur.
Ajax.Responders.register({ onComplete: function(request, transport, json) { if(!request.success()) { var errorMessage = ['Unknown Error', 'An error occurred, but could not be determined correctly.']; if (transport.responseJSON && transport.responseJSON.error) errorMessage = [transport.responseJSON.error.title, transport.responseJSON.error.message] Growl.Bezel({ title: errorMessage[0], text: errorMessage[1], image: '/images/growl_warning.png', duration: 2 }); } } });
Conclusion
The idea presented here may seem dead simple, and it is. Yet, these few lines of code have saved me a lot of time worrying about errors that rarely occur.