There are two separate Ruby ideas here, and both are worth understanding properly.
1. Ruby Heredoc: <<, <<-, and <<~
A heredoc is Ruby’s syntax for defining a multiline string.
The basic structure is:
variable = <<IDENTIFIERsomemultilinetextIDENTIFIER
The identifier (TEXT, SQL, HTML, JSON, etc.) is not special. It is simply a marker that tells Ruby:
“The string starts here and ends when you encounter this exact marker.”
For example:
message = <<TEXTHelloWorldTEXT
The resulting string is:
HelloWorld
The interesting part is what happens to the closing marker’s indentation and the indentation of the content.
2. <<TEXT – Traditional Heredoc
Consider:
message = <<TEXTHelloWorldTEXT
The closing TEXT must start at the beginning of the line.
This works:
def message <<TEXTHelloWorldTEXTend
But this does not work:
def message <<TEXTHelloWorld TEXTend
Because Ruby expects:
TEXT
at the beginning of the line.
Why?
Ruby needs to recognize the terminating identifier.
Think of:
<<TEXT
as saying:
“Keep reading source code until you find a line containing
TEXTin the required position.”
So:
TEXT
is the terminator.
But:
TEXT
is not the terminator for the normal <<TEXT form.
3. Why is this inconvenient?
Ruby style normally uses indentation.
For example:
class UserMailer def welcome_email body = <<TEXTHelloWelcome to our application.Thank you.TEXT body endend
Notice how ugly this is.
The Ruby code is indented:
class def body =
but the heredoc terminator suddenly jumps all the way to column 1:
TEXT
This is where <<- becomes useful.
4. <<-TEXT – Allow an Indented Terminator
Now:
message = <<-TEXTHelloWorld TEXT
Ruby allows the closing identifier to be indented.
So this is valid:
def message <<-TEXTHelloWorld TEXTend
The important thing is:
<<-changes how Ruby recognizes the terminating marker.
It does not remove indentation from the actual content.
For example:
message = <<-TEXT Hello World TEXT
The resulting string still contains the spaces before Hello and World.
Conceptually:
" Hello\n World\n"
The - means:
“I’m allowing the terminator to appear with indentation.”
It does not mean:
“Remove indentation.”
That’s the major distinction.
5. <<~TEXT – Squiggly Heredoc
Ruby later introduced the squiggly heredoc:
message = <<~TEXT Hello WorldTEXT
Now Ruby does two things:
- Allows the terminator to be indented.
- Removes common leading indentation from the content.
This is why <<~ is extremely useful in modern Ruby/Rails code.
6. Understanding “common indentation”
This part is slightly more subtle.
Consider:
message = <<~TEXT Hello World RubyTEXT
Every line has two spaces:
··Hello··World··Ruby
<<~ removes those two common spaces.
Result:
HelloWorldRuby
So:
puts message
prints:
HelloWorldRuby
7. What if the lines have different indentation?
This is where “common indentation” becomes important.
Consider:
message = <<~TEXT Hello World RubyTEXT
The source indentation is:
··Hello····World··Ruby
The smallest/common indentation is 2 spaces.
Ruby removes those two spaces from every relevant line.
Result:
Hello WorldRuby
So <<~ does not flatten all indentation.
It removes the indentation that is common to the content.
That’s exactly what we want when writing nested code.
8. Why <<~ is better for Ruby source code
Consider:
class UserMailer def welcome_email body = <<~TEXT Hello #{user.name}, Welcome to our application. Regards, The Team TEXT body endend
The source code remains nicely formatted.
The Ruby indentation:
class def body Hello
doesn’t become unwanted indentation in the actual string.
The resulting string is:
Hello John,Welcome to our application.Regards,The Team
That’s the main reason <<~ is so popular.
9. <<- vs <<~: The Critical Difference
This is probably the most important thing to remember.
<<-
text = <<-TEXT Hello World TEXT
Means:
“Allow the closing
TEXTto be indented.”
It doesn’t clean up content indentation.
<<~
text = <<~TEXT Hello World TEXT
Means:
“Allow the closing
TEXTto be indented, and remove common indentation from the content.”
<<
text = <<TEXTHelloWorldTEXT
Means:
“The terminator must appear without indentation.”
A useful mental model:
Terminator Content indentation
<<TEXT strict preserved
<<-TEXT flexible preserved
<<~TEXT flexible normalized
10. What about interpolation?
All of these are still normal Ruby strings, so interpolation works.
name = "Abhilash"message = <<~TEXT Hello #{name}, Welcome to Ruby.TEXT
Result:
Hello Abhilash,Welcome to Ruby.
You can also execute Ruby expressions:
message = <<~TEXT User: #{user.name} Email: #{user.email} Status: #{user.active? ? "Active" : "Inactive"}TEXT
This is one reason heredocs are particularly useful in Rails applications.
11. You can use different terminator names
The terminator doesn’t have to be TEXT.
These are all valid:
<<~TEXT...TEXT
<<~SQL...SQL
<<~HTML...HTML
<<~JSON...JSON
<<~RUBY...RUBY
The name is simply a marker.
Using a meaningful marker improves readability:
sql = <<~SQL SELECT * FROM users WHERE active = trueSQL
is much clearer than:
sql = <<~TEXT SELECT * FROM users WHERE active = trueTEXT
12. A Very Useful Rails Example
You may see this frequently in Rails code:
sql = <<~SQL SELECT users.id, users.email FROM users INNER JOIN orders ON orders.user_id = users.id WHERE users.active = TRUE GROUP BY users.id, users.emailSQL
Without <<~, you would have to deal with the terminator’s indentation.
With <<~, the SQL can visually belong to the Ruby code while still producing clean SQL.
This is also useful for:
render inline: <<~HTML <div class="user"> <h2>#{user.name}</h2> </div>HTML
and:
command = <<~BASH bundle install bundle exec rails db:migrate bundle exec rails serverBASH
The two things worth remembering for ints
Heredocs:
<< # strict terminator<<- # indented terminator allowed<<~ # indented terminator + dedent content
One subtle point: <<~ does not simply call strip on every line. It calculates the indentation that can be removed from the heredoc content and removes that common indentation while preserving deeper relative indentation. That distinction is important when you’re using heredocs for HTML, SQL, YAML, or nested text.