This is part 11 of Ramaze by Example, a tutorial on web development. In Part 10: Cosmetics, I improved the look of our application.
Up to this point, we’ve been assuming users will always behave nicely and in predictable ways. As we all know, this is not a safe assumption in practice, so now we’ll add some validation and error handling to our application. I’ll introduce the changes I made somewhat in reverse order, beginning with error message display.
The Flash Helper
Ramaze::Helper::Flash is useful for displaying one-time messages to users. In your controller, you treat flash as a Hash in which you store Strings (usually). These strings can then be interpolated in views, but not just in handling the current request, but also the next one. To understand this tool and technique better, let’s examine how I make use of it in our todo list application. I’ve added a fail method to the controller:
def fail( message ) flash[ :error ] = message end
It simply takes an argument and assigns it to the :error key of the flash Hash. I call the fail method whenever I want to send an error message to the user. To actually display the message, we use the method flashbox in our layout:
<body> <h1>#{@title}</h1> #{flashbox} #{@content} </body>
Checking the operand
Our check off and delete actions have been assuming that there always exists a task that has an id matching the id parameter given. Now we’re going to check whether this is true, and handle the situation gracefully if the condition false.
Since we’re going to perform this check in more than one spot, we stay DRY by making a single method to do the check, a method we will call from multiple places.
def ensure_task_exists( id ) task = Task[ id ] if task.nil? fail "No task with id #{id}." redirect_referrer end task end
If there is no task in the database with the given id, then we call our fail method to setup an error message, and then redirect the user back to wherever she came from with redirect_referrer. If instead the task exists, we simply return the task to the method caller.
So next, we change the action methods to make use of this utility method. Here’s a diff of the relevant part of the controller:
def check_off( id ) - Task[ id ].check_off + task = ensure_task_exists( id ) + task.check_off redirect Rs( :/ ) end def delete( id ) - Task[ id ].delete + task = ensure_task_exists( id ) + task.delete redirect Rs( :/ ) end
Observe how the error handling and redirection are nicely parcelled away elsewhere, keeping the action code clean and succinct.
Seeing errors in action
Now, run the code:
git checkout 11-error-handling
ruby start.rb
and visit /delete/99999 to see this error handling code work. The important thing to notice here is that even though the error message is set in one action (delete), it is available for usage in the next requested action (index). However, it will not be available to future requests (unless it is set again). That’s why it’s called the “flash” hash: because data only remains for a short while.
Constraining the input
We would like to ensure that task descriptions are not empty. While other developers may choose to constrain data at the model or view (Javascript) level, I’m of the persuasion that one should constrain data as much as possible at the database level, to maximize one’s confidence that the data in the database is as pristine as possible. So I will setup the constraint in the schema:
ALTER TABLE tasks ADD CONSTRAINT minimum_description_length CHECK ( CHAR_LENGTH( description ) > 1 );
After adding this constraint, if you try to enter a description which is less than 2 characters long, it will fail — but not very gracefully. So we add some grace by rewriting our create method:
def create description = h( request[ 'description' ] ).strip begin Task.create( :description => description ) rescue DBI::ProgrammingError => e if e.message =~ /minimum_description_length/ fail 'Please enter an adequate description for the new task.' redirect Rs( :new ) else raise e end end redirect Rs( :/ ) end
As you can see, we are wrapping the Task.create call with a begin/rescue pair so we can handle the exception thrown by the database. The if block inside the rescue clause is used to handle a specific exception, namely a violation of the minimum_description_length constraint which we named and defined earlier. If an exception of this kind is thrown, then we setup an error message with fail and redirect back to the task addition page. If it’s some other kind of exception, we don’t want to swallow it up; instead we re-raise it. If no exception is raised, then the user is redirected to the task list.
Try this out to see for yourself that it works. (Don’t forget to add the constraint first if you haven’t done so already.)
Custom error handler
Up to this point, we’ve worked with errors which we anticipate and handle. In reality, we can never foresee every possible error. If you check out the previous tutorial part’s code and run it (git checkout 10-cosmetics; ruby start.rb), then try to visit a non-existent page, such as http://localhost:9001/foobar, you will see an error message and stack trace, something like this:
No Action found for `/foobar' on MainController /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:274:in `raise_no_action' /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:98:in `default' /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:25:in `send' ...
Ramaze lets you override this error page with one of your own; simply put something in place to handle an error action. As we’ve learned, that means either a controller method, or a view, or both. Here’s the controller method we add:
def error @title = 'Application Error' @e = Ramaze::Dispatcher::Error.current end
Whenever an exception occurs in a Ramaze application, the error action is hit. If no handler is set up by the application programmer, Ramaze will use a default error method.
Ramaze::Dispatcher::Error.current evaluates to the Ruby Exception which has been raised and is causing the error condition. We make it available to our view so we can display information about the exception. Here is our error.xhtml view to go with the error method:
<h2>#{@e.message}</h2> <ul> <?r @e.backtrace.each do |frame| ?> <li>#{frame}</li> <?r end ?> </ul>
It should be relatively clear what the above combination of method and view does, but in case it isn’t for you, git checkout 11-error-handling; ruby start.rb and visit http://localhost:9001/foobar again.
Review
Let’s review what we’ve learned over the final parts of this tutorial:
- Layouts are used to keep view code dry and maintain a consistent look across multiple views.
- A layout is just another view, one which interpolates
#{@content}. @contentcontains the rendering of a view (or, in other words, a layout “wraps” other views).- The
layoutmethod is used in a controller definition to tell Ramaze which view to use as the layout view for a controller. It accepts a Hash argument, with the key-value pair being the site path of the layout view, and an array of controller methods (actions) to apply the layout to. e.g.<br/>layout '/page' => [ :index, :new ] - Layouts can make use of controller instance variables (e.g.
@title) just like normal views. - Static files (CSS, Javascript, images, etc.) are placed in a
public/subdirectory in your application’s file tree. flashis a session-wide Hash.- Data stored in the
flashHash persists only for two requests, making it useful for temporarily storing messages for the user across redirects. flashboxis used in views, and evaluates to an HTML<div>for each key-value pair in theflashHash.redirect_referreris used to redirect a browser back to the referring page.
In the Conclusion, I will wrap up the tutorial and talk about further avenues of exploration and learning.
Related posts:
[...] is the conclusion of Ramaze by Example, a tutorial on web development. In Part 11: Validation and Error Handling, we added a custom error page and handlers for common user [...]