chomp! returns nil when nothing changes
Question
What's the return value of the following Ruby 3 code?
str = "hello\n"
str.chomp!.reverse # => "olleh"
str = "hello"
str.chomp!.reverse # => ???
The correct answer is
-
nil -
It raises
NoMethodErrorCorrect -
"olleh" -
"hello"
Explanation
TL;DR
String#chomp! removes a trailing record separator in place and returns the string, but when there is nothing to remove it returns nil. "hello" has no trailing newline, so str.chomp! returns nil and the chained .reverse becomes nil.reverse, which raises NoMethodError.
Step by step
The first pair works because the string actually changes:
str = "hello\n"
str.chomp! # => "hello"
str # => "hello"
chomp! strips the \n, mutates str in place, and returns the mutated string, so .reverse has a String receiver.
The second pair does not:
str = "hello"
str.chomp! # => nil
No trailing separator, no modification, and by convention chomp! signals "nothing changed" by returning nil. The chain then blows up:
str = "hello"
begin
str.chomp!.reverse
rescue NoMethodError => e
e.message # => "undefined method 'reverse' for nil"
end
The bang convention
This is not a chomp! quirk. Many in-place String and Array mutators return nil when they made no change: chop!, strip!, squeeze!, gsub!, sub!, upcase!, downcase!, uniq!, compact!, flatten!. The nil is a useful "did anything happen?" signal in a conditional, but it makes these methods unsafe to chain.
When you need chaining, use the non-bang version, which always returns a String:
"hello".chomp.reverse # => "olleh"
"hello\n".chomp.reverse # => "olleh"
chomp returns a new string whether or not it removed anything, so the chain is safe in both cases.
Share this quiz
Comments
No comments yet. Be the first.