{"record":{"id":"1609c36abf1e11ea","repo":"ViewComponent/view_component","slug":"cannot-deserialize-unknown-component-hash-comp","errorCode":null,"errorMessage":"Cannot deserialize unknown component: #{hash[\"component_class\"]}","messagePattern":"Cannot deserialize unknown component: #(.+?)","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/view_component/serializable/proxy.rb","lineNumber":14,"sourceCode":"# frozen_string_literal: true\n\nmodule ViewComponent\n  module Serializable\n    # A proxy that wraps a component class and its initialization arguments, deferring\n    # component instantiation until render time. This allows slot calls and other\n    # post-initialize configuration to be captured and replayed, and enables the\n    # proxy itself (rather than a live component instance) to be serialized for\n    # background jobs (e.g. ActiveJob / Turbo Streams).\n    class Proxy\n      # Rebuilds a Proxy from a serialized hash produced by +serialize+\n      def self.deserialize(hash)\n        klass = hash[\"component_class\"].safe_constantize\n        raise ArgumentError, \"Cannot deserialize unknown component: #{hash[\"component_class\"]}\" unless klass\n\n        args = ActiveJob::Arguments.deserialize(hash[\"initialize_args\"] || [])\n        proxy = new(klass, *args)\n\n        Array(hash[\"slot_calls\"]).each do |call|\n          method_name = call[\"method\"].to_sym\n          slot_args = ActiveJob::Arguments.deserialize(call[\"args\"])\n          proxy.public_send(method_name, *slot_args)\n        end\n\n        proxy\n      end\n\n      attr_reader :component_class, :initialize_args, :slot_calls\n\n      def initialize(component_class, *args)\n        if component_class.name.nil?\n          raise UnserializableError, \"Cannot serialize anonymous component class #{component_class.inspect}\"","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/ViewComponent/view_component/blob/9f22c36fa7d7b44098a8ed97472e3b4674ae420f/lib/view_component/serializable/proxy.rb#L1-L32","documentation":"Proxy.deserialize rebuilds a deferred component by calling safe_constantize on the stored component_class string from the serialized hash. If that class name no longer resolves — renamed, removed, namespaced differently, or not loadable in the worker process — safe_constantize returns nil and deserialize raises ArgumentError ('Cannot deserialize unknown component: <name>'). This typically surfaces inside a background job or Turbo Stream delivery after a deploy.","triggerScenarios":"A serialized job payload referencing 'User::CardComponent' after the component was renamed to 'CardComponent'; enqueuing in one deploy and executing in another where the class was deleted; a job worker with different autoload/eager-load configuration where the constant is unavailable; a manually built hash with a typo in component_class.","commonSituations":"Renaming/refactoring component classes while old render_later jobs are still queued; long-lived queues (Sidekiq, DelayedJob) spanning deploys; zeitwerk namespace changes after moving files; multi-process setups where one environment lacks the component.","solutions":["Restore or alias the class name so safe_constantize resolves again (e.g., keep a temporary constant alias during migration).","Drain or discard stale queued jobs that reference the old class name before/after the rename.","Verify the class loads in the worker process: 'User::CardComponent'.safe_constantize should not be nil in the job runner's environment.","Wrap deserialization of stale payloads with a rescue and dead-letter or discard the job (see defense strategy)."],"exampleFix":"# before\n# job payload enqueued pre-rename: {\"component_class\" => \"User::CardComponent\", ...}\nViewComponent::Serializable::Proxy.deserialize(payload)\n# -> ArgumentError: Cannot deserialize unknown component: User::CardComponent\n\n# after\n# renamed component gets a temporary alias during migration\nclass CardComponent < ViewComponent::Base; end\nUser::CardComponent = CardComponent # remove after old jobs drain","handlingStrategy":"try-catch","validationCode":"hash = {\"component_class\" => \"User::CardComponent\", \"initialize_args\" => [], \"slot_calls\" => []}\nreturn if hash[\"component_class\"]&.safe_constantize.nil? # drop stale payload early\n\nViewComponent::Serializable::Proxy.deserialize(hash)","typeGuard":null,"tryCatchPattern":"begin\n  proxy = ViewComponent::Serializable::Proxy.deserialize(payload)\nrescue ArgumentError => e\n  raise unless e.message.start_with?(\"Cannot deserialize unknown component\")\n  Rails.logger.warn(\"discarding stale component job: #{e.message}\")\n  nil # caller skips enqueueing/rendering\nend","preventionTips":["Avoid renaming component classes that appear in queued render_later payloads, or drain queues around the deploy.","During migrations, keep a temporary constant alias for the old class name until old jobs are gone.","Validate hash['component_class'].safe_constantize before deserialize when handling payloads of uncertain age."],"tags":["view-component","ruby-on-rails","serialization","activejob","class-not-found","rename-refactor"],"backgroundTag":"class-not-found","analyzedSha":"9f22c36fa7d7b44098a8ed97472e3b4674ae420f","analyzedAt":"2026-08-23T10:20:47.008Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}