Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Rails upgrades

Stop ignoring your Rails (and Ruby) deprecations!

Deprecation warnings might seem minor, but they're gateways to a smoother Rails upgrade. Here's how to manage them efficiently for a smoother transition.
Ali Ibrahim
|
April 23, 2023
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

When working on a Rails upgrade, it can be really easy to overlook deprecation warnings because … they’re just warnings!

In our normal day-to-day work, deprecations silently roll through our logs (if they’re being logged at all). Since we’re all used to ignoring them, teams dive in headfirst to their Rails upgrades without giving deprecations a second thought.

But when teams do this, they’re missing a golden opportunity to ship a big part of their upgrade before ever running bundle update rails.

Deprecations help you upgrade your app before you upgrade your app

There’s nothing wrong with ignoring deprecation warnings in your day-to-day work (I do it too!). But when you start your next Rails upgrade, they should be one of the first things you look at. Typically, deprecation warnings indicate behavior that’s going to be removed from the next version of Rails. For example, on Rails 6.0 you might see this deprecation warning:

Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead.

Since this is just a warning, you can keep using non-symbol keys with config_for while you’re on Rails 6.0. However, once you upgrade to Rails 6.1 this is gonna blow up. So you have two options:

  1. Ignore the deprecation warning and hope that a test catches it when you upgrade to 6.1
  2. Fix the deprecation warning in production today so you have one less thing to think about when you’re ready to upgrade

One of those options sounds a lot more appealing to me. 😎

Lo and behold: the DeprecationSubscriber!

If you’re upgrading a big app, you might see tons of deprecations littering your logs. And it can be hard to keep track of how many there are or who’s fixing what. Let’s see if we can add some tooling to help us with both of these things.

First, Rails provides a few config options for deprecation handling. We’ll set our app to :notify deprecations in all environments.

module MyAwesomeApp < Rails::Application
  config.active_support.deprecation = :notify
end

Now when we hit a deprecation in our app, Rails will send a deprecation.rails event via ActiveSupport::Notifications.

Next, we’ll set up a subscriber to process those events. Lets call it DeprecationSubscriber.

class DeprecationSubscriber < ActiveSupport::Subscriber
  class UnallowedDeprecation < StandardError
    def initialize(message)
      super("Unallowed deprecation found. Please fixt it.\n#{message}")
    end
  end
  
  attach_to :rails

  def deprecation(event)
    exception = UnallowedDeprecation.new(event.payload[:message])
    exception.set_backtrace(event.payload[:callstack].map(&:to_s)
    raise exception
  end
end

Since DeprecationSubscriber inherits from ActiveSupport::Subscriber, we can use attach_to and define a method called deprecation that will automagically receive the deprecation.rails events.

class DeprecationSubscriber < ActiveSupport::Subscriber
  # …
  attach_to :rails

  def deprecation(event)
    # …
  end
end

When DeprecationSubscriber gets a rails.deprecation event, it will raise it as an exception.

  def deprecation(event)
    exception = UnallowedDeprecation.new(event.payload[:message])
    exception.set_backtrace(event.payload[:callstack].map(&:to_s)
    raise exception
  end

Now when we do something like run our test suite, we’ll see errors whenever there’s a deprecation. If you’re still with me you might be asking yourself, “why are we writing all of this code when we could just configure the app to raise errors for deprecations?” If we did that, we’d be putting ourselves in a little bit of a corner because all those deprecations we’ve been ignoring would immediately become errors. And we don’t want to ship a bunch of errors to production. Especially when they’re really just warnings.

So let’s modify DeprecationSubscriber a bit.

class DeprecationSubscriber < ActiveSupport::Subscriber
  class UnallowedDeprecation < StandardError
    def initialize(message)
      super("Unallowed deprecation found. Please fixt it.\n#{message}")
    end
  end
  
  attach_to :rails

  ALLOWED_DEPRECATIONS = [
    "A message for some deprecation",
    "Another message for a different deprecation",
    …
  ]

  def deprecation(event)
    return if ALLOWED_DEPRECATIONS.any? { |allowed| event.payload[:message].include?(allowed) }

    exception = UnallowedDeprecation.new(event.payload[:message])
    exception.set_backtrace(event.payload[:callstack].map(&:to_s)
    raise exception
  end
end

We’ve now added an ALLOWED_DEPRECATIONS array, and we’re using it as a guard clause in #deprecation. If DeprecationSubscriber comes across a deprecation that’s allowed, it’ll be ignored. Otherwise it’ll be raised as an error.

Now when we run our test suite, we’ll collect all of the deprecations that are causing test errors and add them to ALLOWED_DEPRECATIONS. For example, if we see that deprecation about accessing hashes from config_for with non-symbol keys, we’ll add it to the list.

  ALLOWED_DEPRECATIONS = [
    "Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead.",
    …
  ]

And we’ll do this for all the deprecations we come across. We can now ship this to production because we won’t be turning every deprecation into an error. Err, actually we need to make one more change to #deprecation.

  def deprecation(event)
    return if ALLOWED_DEPRECATIONS.any? { |allowed| event.payload[:message].include?(allowed) }

    if Rails.env.development? || Rails.env.test?
      exception = UnallowedDeprecation.new(event.payload[:message])
      exception.set_backtrace(event.payload[:callstack].map(&:to_s)
      raise exception
    else
      Rails.logger.warn("Unallowed deprecation found\n#{event.payload[:message]}")
    end
  end

We’re adding all the deprecations we know about to ALLOWED_DEPRECATIONS, but it’s hard to be 100% sure we’ve added them all. Instead of having these unknown deprecations become errors in production, we’ll log them. After we ship the DeprecationSubscriber to production, we’ll review the logs to collect any deprecations we missed.

After we’ve added all the deprecations we know about to ALLOWED_DEPRECATIONS, we’re ready to ship the DeprecationSubscriber to production.

Burning down ALLOWED_DEPRECATIONS

We now have a documented list of all the deprecations in our app. We can divvy them up across our team, opening small PRs along the way to incrementally bring our app one step closer to running on the next version of Rails. And when deprecations slip through the cracks, they’ll be logged in production giving us one last fail-safe to make sure we’ve really fixed all those deprecations.

As we fix deprecations, we’ll remove them from ALLOWED_DEPRECATIONS. When this happens, developers who naturally ignore deprecations (🙋🏾‍♂️ like me) will see errors pop up while they’re working that tell them it’s time to write non-deprecated code. The computers will help everyone slowly adopt the new patterns, and we’ll be one step closer to shipping that Rails upgrade … all before even running bundle update rails! 🙌🏾

BONUS: Capture Ruby 2.7 positional and keyword argument deprecations

Ruby 3.0 introduces a pretty big change to how positional and keyword arguments work. To support apps migrating from Ruby 2, Ruby 2.7 emits warnings when executing code that won’t work in Ruby 3.0. You can then use these warnings to update your code that’s running on Ruby 2.7 so it’s ready for Ruby 3.0. Sound familiar?

When we’re upgrading Rails apps from Ruby 2.7 to Ruby 3.0, we can leverage the DeprecationSubscriber to collect all of our app’s Ruby 2.7 argument warnings. We can then burn down the list, shipping small changes on Ruby 2.7 that gets our app one step closer to working on Ruby 3.0.

First, we’ll make sure the Ruby warnings are being sent by updating our app’s config.

module MyAwesomeApp < Rails::Application
  Warning[:deprecated] = true
end

Then, we’ll write a small patch to Ruby’s Warning module that turns the Ruby 2.7 warnings into ActiveSupport::Deprecations.

module CaptureRubyWarnings
  RUBY_2_7_DEPRECATIONS = [
    "Using the last argument as keyword parameters is deprecated",
    "Passing the keyword argument as the last hash parameter is deprecated",
    "Splitting the last argument into positional and keyword parameters is deprecated",
  ]

  def warn(message)
    if RUBY_2_7_DEPRECATIONS.any? { |warning| message.include?(warning) }
      ActiveSupport::Deprecation.warn(message)
    else
      super
    end
  end
end

Warning.extend(CaptureRubyWarnings)

[Note: In addition to capturing keyword and positional argument warnings, you might want to capture warnings for other things that are changing in Ruby 3.0. For example, Ruby 3.0 removes URI.escape and URI.unescape.]

Before turning the warnings into ActiveSupport::Deprecations, we’ll first make sure they’re the Ruby 2.7 warnings we care about. Otherwise, we’ll see a lot of noise from all the different Ruby warnings in our app (we should probably fix those warnings too but that’s a story for another day).

Now whenever we run code that won’t work in Ruby 3.0, the DeprecationSubscriber will capture it and raise it as an error. Then we’ll be back in our old workflow, collecting errors and fixing them one bit at a time. 😄

Your monthly dose of better dev

Test Double Dispatch: hot takes, smart tools, and coding shortcuts to make your life easier—delivered every month.

Subscribe now

Related Insights

🔗
Zero downtime Rails upgrades
🔗
Why it’s worth outsourcing your Rails upgrades
🔗
Managing Ruby hashes in Rails upgrades

Explore our insights

See all insights
Leadership
Leadership
Leadership
Audentes Fortuna Iuvat: being bold amidst uncertainty

What should businesses do in the face of uncertainty? This is not the first time leaders are dealing with strange economic environments, and it won’t be the last.

by
Ed Frank
Developers
Developers
Developers
LLMallard: the low-key AI chat bot you secretly need

The most perfect dev workflow for taking advantage of deeply powerful AI tooling that’s hyper efficient on token usage with minimal API calls—and the perfect pair programming partner.

by
Daniel Huss
Leadership
Leadership
Leadership
Why we coach the system, not just the team

Slow delivery isn’t usually about your people—it’s about your system. Shifting focus to incremental improvements in the system helps change not just processes but behaviors for lasting change.

by
Doc Norton
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
No items found.
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.

Your monthly dose of better dev

Test Double Dispatch: hot takes, smart tools, and coding shortcuts to make your life easier—delivered every month.

Subscribe now