
Strict locals let a Rails partial declare exactly which local variables it accepts via a magic comment: <%# locals: (title:, icon: nil) %>. Introduced in Rails 7.1 and unchanged through Rails 8, they give partials a method-like signature — omitting a required local raises missing local:, passing an undeclared one raises unknown local:.
This guide covers the strict locals syntax and every error it raises. All examples and error messages are reproduced on Rails 8.1 (actionview 8.1.3.1, Ruby 3.4). If you landed here mid-debug, jump straight to the error reference.
Rails Partials and Local Variables
Without strict locals, a partial accepts any locals you pass and turns each one into a variable:
<%# app/views/application/_badge.html.erb %>
<ui-badge>
<p>
<%= title %>
<%= tag.i(class: icon) if icon %>
</p>
</ui-badge>Nothing in this file says which locals it needs, or which are optional. Worse, a local you don’t pass isn’t nil — the variable doesn’t exist at all. Render the partial with only a title:
<%= render "application/badge", title: "New" %>The icon reference blows up:
ActionView::Template::Error (undefined method 'icon' for an instance of #<Class:0x00007f7413df7e40>):Before Rails 7.1, the workaround was the local_assigns hash, which holds every local passed to the render call:
<%# app/views/application/_badge.html.erb %>
<% icon = local_assigns[:icon] %>
<ui-badge>
<p>
<%= title %>
<%= tag.i(class: icon) if icon %>
</p>
</ui-badge>That works, but the partial’s interface stays invisible. Strict locals make it explicit.
What Are Strict Locals in Rails?
Starting in Rails 7.1, a magic comment at the top of a partial declares its locals, using the same syntax as Ruby keyword arguments:
<%# app/views/application/_badge.html.erb %>
<%# locals: (title:, icon: nil) %>
<ui-badge>
<p>
<%= title %>
<%= tag.i(class: icon) if icon %>
</p>
</ui-badge>title: is required. icon: nil is optional and defaults to nil, so the local_assigns dance disappears. Anyone skimming the file sees the partial’s full interface on one line:
<%= render "application/badge", title: "New" %>
<%= render "application/badge", title: "Beta", icon: "sparkles" %>Two other signature forms are useful:
<%# locals: (**attrs) %>The double splat accepts any locals and collects them into an attrs hash — handy for wrapper partials that forward options.
<%# locals: () %>An empty signature seals the partial. Passing any local raises an error with the message no locals accepted for app/views/application/_sealed.html.erb.
The enforcement errors changed class between versions. Rails 7.1 raises a plain ArgumentError. Current Rails raises ActionView::StrictLocalsError, a subclass of ArgumentError. Either way, when the failure happens inside view rendering, Rails wraps it in ActionView::Template::Error — that’s the class you’ll see in logs and error trackers.
Strict Locals Errors: Causes and Fixes
Every message below is copied from a real Rails 8.1.3.1 exception, so you can match them against your logs character for character.
missing local: :title
ActionView::Template::Error (missing local: :title for app/views/application/_badge.html.erb):Cause: The partial declares title: as required, and a render call didn’t pass it:
<%# app/views/application/_badge.html.erb %>
<%# locals: (title:, icon: nil) %><%= render "application/badge" %>Fix: Pass the local at the call site:
<%= render "application/badge", title: "New" %>Or, if the partial should work without it, give it a default in the signature:
<%# locals: (title: "Untitled", icon: nil) %>The message names the offending partial, but not the caller. The caller is the frame directly above the partial in the backtrace — that’s where the fix goes.
unknown local: :icon
ActionView::Template::Error (unknown local: :icon for app/views/application/_chip.html.erb):Cause: A render call passes a local the signature doesn’t declare. Strict locals reject extras instead of ignoring them:
<%# app/views/application/_chip.html.erb %>
<%# locals: (title:) %>
<span class="chip"><%= title %></span><%= render "application/chip", title: "New", icon: "star" %>Fix: Either declare the local (with a default if it’s optional):
<%# locals: (title:, icon: nil) %>Or remove it from the render call. Check for typos, too: passing titel: instead of title: produces an unknown local: for the misspelled name. A partial declared with () rejects every local with no locals accepted instead.
ActionView::MissingTemplate and render partial: nil
Two related failures happen when nil reaches render, and Rails 8 raises a different error for each.
A literal render partial: nil (or rendering a record variable that turns out to be nil) raises an ArgumentError:
ActionView::Template::Error ('nil' is not an ActiveModel-compatible object. It must implement #to_partial_path.):The classic ActionView::MissingTemplate appears when nil sneaks into an interpolated partial name, so Rails searches for a path like settings/ that no template matches:
<%# params[:tab] is nil here %>
<%= render partial: "settings/#{params[:tab]}" %>ActionView::MissingTemplate (Missing partial settings/ with {locale: [:en], formats: [:html, :text, :js, :css, :ics, :csv, :vcf, :vtt, :md, :png, :jpeg, :gif, :bmp, :tiff, :svg, :webp, :mpeg, :mp3, :ogg, :m4a, :webm, :mp4, :otf, :ttf, :woff, :woff2, :xml, :rss, :atom, :yaml, :multipart_form, :url_encoded_form, :json, :pdf, :zip, :gzip, :turbo_stream], variants: [], handlers: [:raw, :erb, :html, :builder, :ruby, :jbuilder]}.):Rails appends the list of view paths it searched. A misspelled or nonexistent partial name raises the same error with the bad name in place, like Missing partial application/_settings.
Fix: Never interpolate unchecked values into partial names. Allowlist them:
<% tab = %w[profile billing].include?(params[:tab]) ? params[:tab] : "profile" %>
<%= render partial: "settings/#{tab}" %>And guard record renders that can legitimately be empty:
<%= render @featured_post if @featured_post %>missing local: :post_counter in Collection Renders
ActionView::Template::Error (missing local: :post_counter for app/views/posts/_post.html.erb):Cause: Collection renders inject extra locals — post_counter and post_iteration for a _post partial. If you declare one of them as required, every single-object render of that partial fails, because those locals only exist inside a collection render:
<%# app/views/posts/_post.html.erb %>
<%# locals: (post:, post_counter:) %>
<li><%= post_counter %>: <%= post.title %></li><%# Works %>
<%= render @posts %>
<%# Raises missing local: :post_counter %>
<%= render "posts/post", post: @post %>Fix: Declare the injected locals with defaults, so both render paths work:
<%# locals: (post:, post_counter: 0, post_iteration: nil) %>The inverse gotcha existed before Rails 7.1.2: rendering a collection against a partial that declared only (post:) failed with unknown keywords: :post_counter, :post_iteration (verified on Rails 7.1.1). That was fixed in Rails 7.1.2, and on Rails 8 a plain (post:) signature handles collection renders fine.
unknown local: :request_id in Turbo Stream Broadcasts
ActionView::Template::Error (unknown local: :request_id for app/views/comments/_comment.html.erb):Cause: turbo-rails 2.0.0 through 2.0.7 merged a request_id local into every Turbo Stream broadcast render — but only when a request id was present. That made it a production-shaped bug: broadcasts worked from the console (no request id), then failed the moment a real request triggered the callback:
class Comment < ApplicationRecord
belongs_to :post
after_create_commit -> { broadcast_append_to(post, target: "comments") }
end<%# app/views/comments/_comment.html.erb %>
<%# locals: (comment:) %>
<p><%= comment.body %></p>Fix: Upgrade turbo-rails to 2.0.8 or later, where the injected local is gone — on 2.0.23, the partial above broadcasts cleanly with only (comment:) declared. If you’re pinned to an older version, declare the local with a default:
<%# locals: (comment:, request_id: nil) %>Strict-locals errors like missing local: and ActionView::MissingTemplate typically only surface in production, when an edge-case render path finally runs. AppSignal’s Rails error tracking groups these ActionView exceptions with the full backtrace and request parameters, so you can see exactly which caller rendered the partial wrong.
Strict Locals with Collections and Broadcasts
These two errors share a root cause worth understanding: some render paths pass locals you never wrote at a call site.
Implicitly Rendering a Collection
Rendering a collection implicitly resolves each record to its partial:
<%= render @posts %>For each post, Rails passes post, plus post_counter (the zero-based index) and post_iteration (an object with first?, last?, index, and size). With strict locals, only the locals you declare are set. This signature supports every render path:
<%# app/views/posts/_post.html.erb %>
<%# locals: (post:, post_counter: 0, post_iteration: nil) %>
<li><%= post_counter %>: <%= post.title %></li>If the partial never uses the counter, don’t declare it — on Rails 7.1.2 and later, (post:) alone works for both single and collection renders.
Broadcasting Over ActionCable
Turbo’s model broadcasts render partials outside any request, from callbacks or background jobs:
class Comment < ApplicationRecord
belongs_to :post
after_create_commit -> { broadcast_append_to(post, target: "comments") }
endturbo-rails injects one default local: the model itself, under its element name (comment: here). So a strict (comment:) signature matches what the broadcast passes, and nothing else is needed on current turbo-rails. The request_id injection that broke this on 2.0.0–2.0.7 is covered in the error reference.
The broader lesson: a partial rendered through framework machinery (collections, broadcasts, mailers) receives locals that machinery decides on. When you add strict locals to such a partial, its signature must account for every one of those render paths.
When to Use Strict Locals
Strict locals earn their keep in partials with several locals, especially optional ones. A user card that shows extra details to admins is a perfect case:
<%# locals: (name:, email:, last_signed_in: nil) %>
<ui-card>
<dl>
<dt>Name</dt>
<dd><%= name %></dd>
<dt>Email</dt>
<dd><%= email %></dd>
<% if last_signed_in %>
<dt>Last signed in at</dt>
<dd><%= last_signed_in %></dd>
<% end %>
</dl>
</ui-card>One line documents the interface, and the optional last_signed_in needs no local_assigns fallback.
The value is lower for a model’s conventional partial (posts/_post.html.erb), where the single post local is obvious from the file name. Declaring it is harmless, but it’s not where strict locals shine.
In new apps, use them freely. In legacy apps, add them partial by partial, and only where your tests exercise every render path — these errors appear at render time, not boot time.
A Few Things to Keep in Mind
Adding strict locals to a partial is all-or-nothing for that file. The moment the magic comment exists, every local must be declared; you can’t enforce some locals and let others through. That’s why framework-injected locals (counters, broadcast defaults) trip people up.
local_assigns still works alongside strict locals, so you can keep using patterns like local_assigns.fetch(:icon, nil) during a migration.
An undeclared render path in a rarely-hit branch won’t fail until production traffic reaches it. Make sure those exceptions get reported with context — AppSignal’s exception handling for Ruby covers wiring that up.
Finally, remember that this magic comment carries behavior. It reads like documentation, but changing it changes which renders raise. Treat signature edits like method signature changes and check every caller.
Wrapping Up
In this post, we covered strict locals from Rails 7.1 through Rails 8: the signature syntax with required, defaulted, and splat locals, and the exact errors enforcement raises — missing local:, unknown local:, the nil partial failures, collection counters, and the historical turbo-rails broadcast clash.
Happy coding!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- How do I fix missing local: :title in a Rails partial?
- The partial declares title as a required local in its locals magic comment, but the render call did not pass it. Pass title at the call site, or give it a default value in the signature, like title: nil.
- What causes unknown local: :icon in Rails strict locals?
- The render call passes a local that the partial’s magic comment does not declare. Rails rejects undeclared locals once strict locals are enabled. Declare the local in the signature, with a default if it is optional, or stop passing it.
- Why does render partial: nil raise ActionView::MissingTemplate?
- A nil usually reaches render through an interpolated partial name, producing a path like posts/ that matches no template, so Rails raises MissingTemplate. A literal render partial: nil raises ArgumentError instead on Rails 8. Guard or allowlist dynamic partial names.
- Why does a Rails partial raise missing local: :post_counter?
- The partial declares post_counter as required, but counter locals only exist when Rails renders the partial with a collection. A single render then omits it. Declare post_counter: 0 and post_iteration: nil as defaults so both render paths work.
- Why do Turbo Stream broadcasts fail with unknown local: :request_id?
- turbo-rails 2.0.0 through 2.0.7 injected a request_id local into broadcast renders made during requests, and strict locals reject undeclared locals. Upgrade to turbo-rails 2.0.8 or later, or declare request_id: nil in the partial’s signature.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Ayush Newatia
Guest author Ayush is a freelance Ruby and Rails developer. He's the author of The Rails and Hotwire Codex and part of the Bridgetown core team. He also runs a privacy focused mailing list app called Scattergun.
All articles by Ayush NewatiaBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


