Herb on Rails

Herb on Rails

ERB does not know that it is generating HTML. It finds the <% %> tags, evaluates the Ruby inside them, and concatenates everything else as plain text. Whether the result is valid markup has never been its problem.

Herb changes that. On August 25, 2026, the Ruby on Rails core team merged Add Herb as an HTML-aware ERB implementation opens a new window , which brings in Herb opens a new window , an ERB implementation that parses HTML and ERB into a single syntax tree and uses Prism opens a new window for the Ruby inside the tags. Broken markup can now fail while the template compiles, instead of reaching a browser.

In this article, you will learn what Herb is, how to audit your own views with it today, what it can fix for you, what it cannot, and what it costs to run. We used the FastRuby.io views as the test subject.

What Herb actually is

Herb is not one tool, it is a parser with a lot of things built on top of it. The parser is written in C, it handles HTML and ERB together, and it hands the Ruby inside the tags to Prism. Everything else in the project is a consumer of that one syntax tree. We have leaned on syntax trees for this kind of work before, when we extracted every deprecation warning out of the Rails source opens a new window , and the appeal is the same: a parser knows things a regular expression can only guess at.

That second half is easy to overlook. Every <% %> tag in your application gets parsed as Ruby, including the ones inside branches that almost never render. GitHub found invalid Ruby this way while adopting Herb opens a new window , in template code they had never run outside of production. If your views have accumulated conditionals for states nobody has hit in years, that Ruby has probably never been parsed by anything.

The gem installs a CLI that exposes most of it:

Command What it does
herb analyze Parse and compile every template in a directory, then report
herb lint Rule-based linting, including autocorrection
herb format Reformat templates
herb playground Open a template’s parse tree in the browser playground
herb report Generate a bug report you can paste into an issue
herb diff Show the minimal set of syntax tree differences between files
herb dev Start a dev server that watches for changes
herb actionview check Check that render calls resolve to real partials
herb actionview graph Show the render dependency graph
herb lsp Start the language server for your editor

Some of those delegate to Node packages under the hood, so the linter and formatter are the same code whether you reach them from Ruby or from npx.

That shared parser is the part worth understanding, because it is what makes the Rails change more than a swapped dependency. Your editor, your linter, your formatter, and now your template compiler can all agree on what a template means.

Herb v0.9 opens a new window added a set of erb-safety-* rules that cover the security checks from better-html and erb_lint, which matters if you have been relying on either of those.

Running Herb on our own views

Install the gem and point it at your views:

gem install herb
herb analyze app/views

It parses every template, tries to compile each one, and groups what it finds into sections. The ten possible sections are in the table below, and which ones you see depends on what is wrong with your templates:

Seen Section What it means
Template errors Real problems in your markup
Strict mode parse errors Valid but loose HTML, like omitted closing tags
Analyze parse errors Problems found while analyzing the tree
Validation errors Security, nesting, or accessibility issues; the template still compiles
Unexpected parse errors Probably a parser bug
  Strict mode compilation errors Compiles only with strict mode off
  Parser crashed The parser itself blew up
  Timed out The file took too long to parse
  Compilation errors Could not be compiled to Ruby at all
  Invalid Ruby output The engine emitted Ruby that does not parse

Our run against the FastRuby.io views identified the five marked in the first column, along with twenty-one templates containing issues. This is a reasonable outcome for an application whose views have been accumulating since 2017.

What caught my attention was the “Reportable issues” block Herb prints after those sections. It pulls out the files that landed in the categories it treats as probable bugs in itself, under a note saying they “likely failed due to issues in Herb, not in your templates”. Two of ours landed there, both from the unexpected parse errors row. That seems like a good way to make the Herb gem more robust over time, and the CLI hands you both halves of the job, one to share as an issue and the other to debug it yourself:

herb report prints a Markdown bug report with the gem, Prism, and libherb versions, your Ruby and platform, the categorized error list, and the template source, ready to paste into an issue.

herb playground compresses the template into the fragment of a URL and opens it in the playground opens a new window , where you can inspect the tree the parser built and the diagnostics it produced. Browsers do not send the fragment part of a URL to the web server, so the template travels no further than your own browser, which matters when the file belongs to a client.

As for the errors themselves, they are readable. Here is one of ours:

Analyze parse errors:
 These files have issues detected during analysis. Review the errors and update your templates.

 app/views/static/rails_3_0_vulnerabilities.html.erb:
   ⚠ MissingClosingTagError at 18:2 - Opening tag `<section>` at (18:3) doesn't have a matching closing tag `</section>` in the same scope.
   ⚠ MissingOpeningTagError at 2131:2 - Found closing tag `</section>` at (2131:4) without a matching opening tag in the same scope.
   ⚠ MissingOpeningTagError at 2132:0 - Found closing tag `</div>` at (2132:2) without a matching opening tag in the same scope.

The run ends with a summary that counts how many files were checked, how many came out clean, and separate tallies for the parser and the engine. Those two tallies are worth reading separately, since “parses but will not compile” and “will not parse at all” are different problems with different fixes.

What you can fix automatically

herb analyze tells you what is broken. Fixing the smaller stuff is the linter’s job:

herb lint "app/views/**/*.erb"
herb lint "app/views/**/*.erb" --fix

On our views that found 1,404 offenses, 547 of them autocorrectable: unescaped entities, indentation, spaces inside tags, missing trailing newlines. What --fix will not touch is anything structural. The 143 offenses from parser-no-errors, the rule that overlaps with what stops a template from compiling, are all yours to fix by hand, and that is the right call, since an extra </div> could mean “delete this line” or “add the opening tag twenty lines up”.

Both commands run on defaults until you give them a config, which you generate with:

herb lint --init

That writes a .herb.yml file that pins the Herb version, so a later upgrade does not enable new rules behind your back. The rest of the file is commented out but worth reading through once. You can enable or disable individual rules, exclude paths for specific rules, set the severity that causes the run to fail, toggle the security, nesting, and accessibility validators, and choose the template engine, with herb as one of the options.

What you get, and what it costs

From your application’s rendering point of view, Herb and Erubi are the same thing: same public API, same job, no templates to rewrite.

What changes is the tooling around them. Because the herb gem is now a dependency of actionview, the analysis, the linter, the formatter, the render graph, and the language server all come along with it. Some of that existed before as third-party gems, erb_lint and better-html among them, but none of it shipped with Rails, so nobody has to be talked into adding a gem for good practices.

The review thread points at Herb arriving opt-in first and becoming a framework default in some later version. That is reason enough to get ahead of it. Run the linter now, check that your views compile through Herb, and wire it into CI alongside RuboCop or whatever else you already run there, so the work happens before the release day instead of after it. A missing closing tag stops being something a reader finds for you months later.

The trade is compile time. Herb does more work than Erubi, so a template takes longer to compile, and on our views the difference is measured in milliseconds per template.

Where that lands is the part worth understanding. Rails compiles a template the first time something renders it and caches the compiled method for the life of the process, so the cost is paid once per template. Every request after that is identical, because the Ruby that Herb generates is the same Ruby, byte for byte. Nothing about serving a page is slower once the template has been compiled.

It is also early days. Marco Roth, who built Herb, has noted that it has not had much performance optimization yet, and that he believes it can be improved.

Conclusion

We pointed Herb at an application whose views have been accumulating since 2017, and it found real problems in twenty-one templates, including markup we had been shipping for years. The run took a few seconds and needed nothing beyond a gem install.

That is what we would suggest doing now, well before any of this becomes a default. Views are where upgrade work tends to hide, and we have written before about ERB syntax changes catching teams mid-upgrade opens a new window . Herb has earned a spot next to the open source tools we already reach for on upgrades opens a new window . The linter will clear the cosmetic offenses for you, the structural ones are hand work, and knowing which is which is most of the planning. Once you have cleared them, both herb analyze and herb lint exit non-zero when something fails, so either one can sit in CI and keep the next broken template out of the branch.

Marco Roth is presenting Herb in Rails 8.2: Your ERB views, now HTML-aware opens a new window at Rails World on September 24.

Is your team getting ready for the next version of Rails, or still catching up on the last one? We can help! opens a new window

Get the book