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 , which brings in Herb , an ERB implementation that parses HTML and ERB into a single syntax tree and uses Prism 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 , 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 , 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 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 , 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 . Herb has earned a spot next to the open source tools we already reach for on upgrades . 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 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!