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

  1. Remove the block and pass content via serializable slot calls: MyComponent.render_later("title").with_item(label: "One").
  2. 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).
  3. 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

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


AI-assisted analysis of ViewComponent/view_component@9f22c36fa7 (2026-08-23). Data as JSON: /api/errors/71db0fccc8720fbe. Report an issue: GitHub.