ViewComponent/view_component · error · ViewComponent::Serializable::UnserializableError
Cannot serialize render_later with a block
Error message
Cannot serialize render_later with a block
What it means
Component.render_later (ViewComponent::Serializable) returns a Proxy that defers instantiation until render time and can be handed to ActiveJob / Turbo Streams, which serialize it to a plain hash. Ruby blocks cannot be serialized, so render_later raises ViewComponent::Serializable::UnserializableError (an ArgumentError subclass) immediately if a block is passed. Slot data must instead be supplied through serializable slot-call arguments (with_* methods without blocks).
Source
Thrown at lib/view_component/serializable.rb:20
require "active_support/concern"
require "view_component/serializable/proxy"
module ViewComponent
module Serializable
extend ActiveSupport::Concern
class UnserializableError < ArgumentError; end
class_methods do
# Returns a Proxy that captures this class and its initialization arguments,
# deferring instantiation until render time. The proxy is renderable and
# serializable for ActiveJob (e.g. Turbo Streams). Slot calls made on the
# proxy are captured and replayed at render time. Blocks are not supported.
#
# MyComponent.render_later("title", size: :large).with_item(label: "One")
def render_later(*args, &block)
raise UnserializableError, "Cannot serialize render_later with a block" if block
ViewComponent::Serializable::Proxy.new(self, *args)
end
ruby2_keywords :render_later
end
end
end
View on GitHub (pinned to 9f22c36fa7)
Solutions
- Remove the block and pass content via serializable slot calls: MyComponent.render_later("title").with_item(label: "One").
- If content must be computed now, capture it into a string/variable first and pass it as a slot or initializer argument (plain data serializes fine).
- If a block is essential (content needs the view context at render time), render synchronously with MyComponent.new(...).render_in(view_context) instead of render_later.
Example fix
# before
proxy = MyComponent.render_later("title", size: :large) do
"<p>Block content</p>"
end # UnserializableError
# after
proxy = MyComponent.render_later("title", size: :large).with_item(label: "One") Defensive patterns
Strategy: validation
Validate before calling
def deferred(component_class, *args, &block) raise ArgumentError, "blocks are not supported by render_later" if block component_class.render_later(*args) end
Try / catch
begin
proxy = MyComponent.render_later("title")
rescue ViewComponent::Serializable::UnserializableError
# fall back to synchronous render when a block/content cannot be deferred
render(MyComponent.new("title"))
end Prevention
- Treat render_later as data-only: pass strings/numbers/hashes, never blocks or Procs.
- Wrap deferred calls in a small helper that rejects blocks loudly at the call site.
- Convert block content into a slot argument before enqueueing.
When it happens
Trigger: MyComponent.render_later { "content" }; appending a content block like render_later(args) { |c| ... }; migrating synchronous render(MyComponent.new) { ... } calls to the deferred API without removing the block.
Common situations: Moving inline Turbo Stream broadcasts into background jobs and carrying over block-style content; templates that pass a block when rendering; habit from render/render_in which both accept blocks.
Related errors
- Cannot deserialize unknown component: #{hash["component_clas
- Cannot serialize render_in with a block
- Cannot serialize slot call '#{method_name}' with a block
- Cannot serialize anonymous component class #{component_class
- Inline templates can only be defined once per-component.
AI-assisted analysis of ViewComponent/view_component@9f22c36fa7 (2026-08-23).
Data as JSON: /api/errors/71db0fccc8720fbe.
Report an issue: GitHub.