save bang your head, active record will drive you mad
Posted by jcarroll
Sep 25
So in my never-ending quest to remove conditional logic from code, I began writing my Rails actions like:
1 2 3 4 5 6 7 8 |
def create @user = User.new params[:user] @user.save! redirect_to user_path(@user) rescue ActiveRecord::RecordNotSaved flash[:notice] = 'Unable to create user' render :action => :new end |
Instead of the traditional way using conditional logic like:
1 2 3 4 5 6 7 8 9 |
def create @user = User.new params[:user] if @user.save redirect_to user_path(@user) else flash[:notice] = 'Unable to create user' render :action => :new end end |
I did this until one of my co-workers saw this and said:
exceptions should not be expected
What?
When writing an application you expect invalid input from users. Since we expect invalid input we should NOT be handling it via exceptions because exceptions should only be used for unexpected situations.
Well what’s an unexpected situation?
- Losing a connection to your database.
- Running out of memory
- Some obscure IO/socket error
Now I’m not going to write error handling for those unexpected circumstances because I don’t expect them to happen. If they do then I want the user to see an error page (500) and the developers to be notified via email about the exception (using exception notifier in Rails) (if i cared then yes I’d have to write error handling code but trying to recover from situations such as an out of memory is not going to be easy, if at all possible, so I want my application to fail in such a rare situation).
Let’s look at an example of reading from a file from the pickaxe.
1 2 3 4 5 |
File.open('testfile') do |file| while line = file.gets puts line end end |
IO#gets returns a String or nil. That is correct because reaching the end of the file is NOT an unexpected situation and should be handled with conditional logic.
What if ruby raised some EOFError instead:
1 2 3 4 5 6 7 |
File.open('testfile') do |file| while line = file.gets puts line end rescue EOFError return end |
Raising an EOFError would mean reaching the end of the file would be unexpected. That’s crazy talk.
I think ActiveRecord::Base#save! and ActiveRecord::Base.update_attributes! should be pulled from the public API. All they do is save the object and raise an exception if any validation fails. I doubt there are any web applications out there that do NOT expect invalid input. So there is no point in using #save! and #update_attributes! over their true/false equivalents #save and #update_attributes.
However, there is one case where they both have to be used. In transactions. Apparently the only way to roll back a transaction is to raise an exception. I say there should be some #rollback method so you could do something like:
1 2 3 4 |
User.transaction do user.save || rollback another_user.save || rollback end |
Following this convention we could also return the ’!’ back to its original meaning of ‘modifying the receiver’. Rails, with its #save! and #update_attributes!, has pushed the ’!’ to mean ‘something dangerous’ in order to justify the exception raising of methods like #save! and #update_attributes!.
Another Ruby idiom that is an abuse of exceptions is something along the lines of:
1 2 3 |
user.address.street rescue '' # or user.address.street rescue nil |
Code like this says that an address is optional for a user. Since a user without an address is not an exceptional situation it should be handled with conditional logic instead of exceptions.
1 2 3 |
if ! user.address.nil? user.address.street end |
I can’t stand that ‘rescue nil’ hack. Quit being lazy and write the ‘if’ statement.
Comments on this post
Sep 26
Ryan Allen said,
Instead of saying
if user.saveI do the following:So, an extra line of code… but I think it reads better.
Sep 26
Todd said,
I generally agree that exception handling for expected cases is not a good idea. I decided to do some benchmarking and it looks like for happy path the inline rescue nil is faster… My tests were really simple… Here’s my results: user system total real Baseline Happy Path Rescue 0.440000 0.010000 0.450000 ( 0.682402) Baseline Happy Path If/Else 0.620000 0.010000 0.630000 ( 0.886856) Not Happy Path Rescue 2.150000 0.150000 2.300000 ( 3.013638) Not Happy Path If/Else 0.240000 0.000000 0.240000 ( 0.394679)
And here’s my test code: require ‘ostruct’ require ‘benchmark’
user = OpenStruct.new({:address => OpenStruct.new({:street => “N st NW”})}) user.address user.address.street user2 = OpenStruct.new({:address => nil })
n = 50000 Benchmark.bm do |x| x.report(“Baseline Happy Path Rescue”) do n.times do user.address.street rescue nil end end x.report(“Baseline Happy Path If/Else”) do n.times do if user.address user.address.street end end end x.report(“Not Happy Path Rescue”) do n.times do user2.address.street rescue nil end end x.report(“Not Happy Path If/Else”) do n.times do if user2.address user2.address.street end end end end
Sep 26
Todd said,
Sorry my code didn’t paste as I expected… here it is again with the correct filter
Sep 26
Todd said,
One more try this time with the numbers easy to read:
Sep 26
sandofsky said,
Exceptions are worse than goto statements, but there are a couple acceptable when validating.
One would be if you’re saving a record you know should be valid.
That’s a lot clearer than using “save!”.
Another time I throw exceptions is when a user is doing something malicious. I treat SecurityError as an emergency flare to get the hell out of there.
A simplified example:
Sep 26
Alexandru Nedelcu said,
I do not agree with your statement.
Exceptions where invented to separate error handling from logic.
Your examples are too simple to illustrate the need for such separation, but when your block of code has a complex logic, manually handling errors with if conditions obfuscates the purpose of the code.
I prefer code clarity over anything else, and the first example you gave is a lot clearer for me.
The real problem is … what to exceptions to check and what to ignore ? I think Java answers this with its checked-exceptions, but by abusing the meaning of a checked-exception done by lots of APIs … it turned into feature bloat.
Sep 26
Tammer Saleh said,
JareCare: This is very well put, and I agree that there’s really no place for #save! and the like if you’re following this rule.
Is much more explicit, without being much more verbose.
Sep 26
Joe Grossberg said,
Principle #34 of The Pragmatic Programmer: “Use Exceptions for Exceptional Problems”
Then again the Pythonistas say “It’s easier to ask forgiveness than permission”.
P.S. I think you mean this: ;)
unless user.address.nil? user.address.street endP.P.S. See you guys on Friday.
Sep 26
Joe Grossberg said,
Other alternatives:
user.address.street unless user.address.nil?user.address.nil? user.address.street : nilSep 26
Joe Grossberg said,
Other alternatives:
user.address.street unless user.address.nil?user.address.nil? ? user.address.street : nilSep 26
Rob Sanheim said,
Rails did not “change the meaning” of the bang to mean something dangerous. Bang methods have always meant that in Ruby, see Kernel#exit! for instance. Matz has even commented on the fact that the bang means ‘danger’, and not necessarily modifies its receiver (though thats often the case).
See also David A Black’s post on this.
Sep 26
Tammer Saleh said,
Joe: Or even
user.address.street if user.addressor
user.address and user.address.streetSep 26
Jon Yurek said,
Quite so! But invalid user input isn’t actually an error. Neither is an EOF. Both are (or at least should be) part of the normal flow of a program. Exceptions are for errors— the times when your program throws up its hands in frustration. It’s a normal thing for your users to type their password confirmation wrong, not a time for panic.
Sep 26
Dan Croak said,
Rob’s right. Ruby is consistent with “the bang method means something dangerous/unexpected/atypical will happen.”
For instance, you expect many methods on Array and String to return a new Array or String. However, on Array#replace, you expect the array to be modified so there is no bang method.
Sep 26
Joe Grossberg said,
Tammer:
user.address and user.address.streetUh, forget that. Gives me flashbacks of ugly Zope/ZPT hacks.
Sep 26
Lee O'Mara said,
ActiveRecord is not used exclusively for web application. As such, its API should not be limited by what is common practice in that domain.
Also, you may be interested in sane_transactions which offers an alternate take to the default AR one.
Sep 27
Barry Hess said,
Several of us in ruby.mn were trying to solve the train wreck problem, but we never did come up with a consensus.
You describe not using this:
but rather:
That gets to be a big annoying mess when you’re trying to hit, say,
user.city.state.name. Just for legibility I’d rather do arescue nilthan:My dislike of that conditional above (and it’s proposed single-line derivatives) is a lot less about lazy than it is about aesthetics. After all, this code is almost exclusively in the view.
We came to one idea that enhanced the
NilandActiveRecord::Base method_missingdefinitions to allow a notation like:The leading ’_’ character would result in a nil-safe series of calls, returning nil if anything in the series was nil. Unfortunately the solution seemed a little twitchy, probably because of Ruby’s existing “underscore” methods.
Another member of the group created a safe-getter class that dealt with the issue.
I’d love to see some more alternatives to solving the trainwreck problem. Until then,
rescue nilreads better than anything else I can come up with.Sep 27
crayz said,
Unless you’re actually planning to run through a save!/rescue loop a few thousand times in your create method, I wouldn’t worry about performance considerations
Sep 27
Jon Yurek said,
There is no problem domain where using #save! is a good method. Exceptions should be used as error control, not normal flow control.
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives