The letter A styled as Alchemists logo. lchemists
Published March 15, 2023 Updated March 15, 2023
Cover
Ruby Warnings

Warnings are what you see when Ruby detects issues with your code that might not cause your application to fail now but could in the future if not properly addressed. Sadly, these warning messages are disabled by default.

I want to help you get into the habit of ensuring warnings are always enabled and a permanent part of your workflow because reducing surprising behavior leads to less stress and having to deal with production issues later when it’s never convenient for you or your customers. This article will teach you how use, listen to, and be proactive with your Ruby warnings. 🚀

Overview

For context, when discussing warnings, I’m specifically talking about Kernel#warn which can be used like this:

warn "A basic warning."
# A basic warning.

warn "Warning A.", "Warning B.", "...etc..."
# Warning A.
# Warning B.
# ...etc...

warn "Level 0", uplevel: 0
# (irb):8: warning: Level 0

warn "Level 1", uplevel: 1
# ~/<redacted>/irb/workspace.rb:113: warning: Level 1

warn "Level 2", uplevel: 2
# ~/<redacted>/irb/workspace.rb:113: warning: Level 2

warn "A deprecation example.", category: :deprecated
# A deprecation example.

warn "An experimental example.", category: :experimental
# An experimental example.

This is a basic overview only, we’ll talk more about how all of this works in the upcoming sections. Before moving on, I want to emphasize the importance of using warnings in your own implementation as a way to inform downstream consumers of changes to your implementation.

Here’s are a few common and practical examples:

# Informs of new and better usage.
warn "Demo#example is deprecated, use Demo#to_s instead.", category: :deprecation

# Informs that this method is unstable and behavior might change in the future.
warn "Demo#future is experimental and not fully fleshed out. Use with caution.",
     category: :experimental

I tend to be very judicious of my use of Kernel#warn by sticking only to category usage. This is most applicable for gems or dependent projects in general as a communication tool. For everything else, I’ll use a Logger and would recommend this practice for you as well.

Now that we have a basic understanding of how to issue warnings, we can spend the rest of this article talking about how to consume and manage warnings.

Command Line Interface (CLI)

The fastest way to experiment with Ruby warnings is via the CLI. We’ll start with the basics and then work our way to more advanced usage.

Basics

You’ll want to start by looking at the help text and running the following (truncated for brevity):

ruby -h

# -w                     turn warnings on for your script
# -W[level=2|:category]  set warning level; 0=silence, 1=medium, 2=verbose

You can also use ruby --help for more detailed output (highly recommend). For instance, you’ll get detailed documentation on the following warning categories:

Warning categories:

deprecated      deprecated features
experimental    experimental features

For even more verbosity, you can read through the Ruby manual page by running the following (truncated for brevity):

man ruby

# -W[level=2]  Turns on verbose mode at the specified level without printing the version message at the beginning. The level can be;
#
# 0  Verbose mode is "silence". It sets the $VERBOSE to nil.
# 1  Verbose mode is "medium". It sets the $VERBOSE to false.
# 2 (default) Verbose mode is "verbose". It sets the $VERBOSE to true.
# -W2 is the same as -w

We’ll focus on levels and categories shortly but first — now that you know what’s possible — let’s experiment with the -w flag. Consider the following where I use the -e option to execute Ruby code where I define the same method twice (not recommended):

ruby -e "def demo = super; def demo = super"

If you run this from the console, you’ll notice nothing happens but let’s pass the -w flag to enable warnings and see what happens:

ruby -w -e "def demo = super; def demo = super"

# -e:1: warning: method redefined; discarding old demo
# -e:1: warning: previous definition of demo was here

Much better! Now you can see the value of having warnings enabled so you can catch bad code and clean it up accordingly. 🎉

Levels

As you saw earlier, there are three levels (i.e. 0, 1, and 2) and don’t forget that -w is the same as -W2. To illustrate, let’s walk through each level using the bad code from earlier:

# Level 0: Turns warnings off so there will be no output.
ruby -W0 -e "def demo = super; def demo = super"

# Level 1: Turns warnings on but only at medium level. We'll not get any output, though.
ruby -W1 -e "def demo = super; def demo = super"

# Level 2: Turns warnings on. This is the default and identical to using the `-w` flag.
ruby -W2 -e "def demo = super; def demo = super"

# -e:1: warning: method redefined; discarding old demo
# -e:1: warning: previous definition of demo was here

Categories

Use of categories is a relatively new feature that was first introduced in Ruby 2.7.0. There are two supported categories which you saw in the help text earlier:

  • Deprecated: Warns when deprecated features are used and will soon be removed.

  • Experimental: Warns when experimental features are used but are not yet fully supported.

Both of the above are worth having turned on because you’ll have insight into where Ruby language features are headed in the future so you can stay on top of these changes. Using the CLI, here’s how to enable and disable these categories:

Defaults

ruby -e 'puts "Deprecated: #{Warning[:deprecated]}, Experimental: #{Warning[:experimental]}"'
# Deprecated: false, Experimental: true

Fully Enabled

ruby -W:deprecated \
     -W:experimental \
     -e 'puts "Deprecated: #{Warning[:deprecated]}, Experimental: #{Warning[:experimental]}"'

# Deprecated: true, Experimental: true

Fully Disabled

ruby -W:no-deprecated \
     -W:no-experimental \
     -e 'puts "Deprecated: #{Warning[:deprecated]}, Experimental: #{Warning[:experimental]}"'

# Deprecated: false, Experimental: false

I don’t like that deprecated warnings are disabled by default so ensure they are globally enabled via my Dotfiles and will show you do this yourself when we talk about the Environment next.

Environment

While the CLI flags are convenient, all that typing does get tedious. Thankfully, Ruby lets you globally configure these flags via the RUBYOPT environment variable. For example, here’s my Dotfiles configuration:

export RUBYOPT='-W:deprecated --yjit'

The above instructs Ruby to ensure deprecation warnings — as mentioned earlier — are enabled along with YJIT. The RUBYOPT environment variable supports many flags which you can learn more about when running man ruby. Here’s a short snippet from the manual page:

Note that RUBYOPT can contain only -d, -E, -I, -K, -r, -T, -U, -v, -w, -W, --debug, --disable-FEATURE and --enable-FEATURE.

Exploring all options is outside the scope of this article. Instead we’ll focus on how you can configure RUBYOPT to support different warning configurations. Here’s a few examples:

# All warnings (including deprecations and experimentals) are enabled.
export RUBYOPT='-w -W:deprecated -W:experimental'

# All warnings (including deprecations and experimentals) are disabled.
export RUBYOPT='-W0 -W:no-deprecated -W:no-experimental'

# Only deprecated warnings are enabled.
export RUBYOPT='-W:deprecated'

If you need to remember what you set for this environment variable, you can always print it out:

printf "%s\n" $RUBYOPT

# -W:deprecated
# --yjit

At this point you might be thinking: "Hey, I thought you said you always enable warnings so why aren’t you setting the -w flag for the RUBYOPT environment variable?" The short answer is I tend to care about warnings that directly effect the projects I’m working on so enable them via my test suite where I have the most agency to take direct action. I’ll show you how to do this using RSpec soon.

Code

Besides being able to configure warnings from the CLI or RUBYOPT environment variable, you can also manipulate your warning settings via Ruby code as follows:

# Level 0: Disabled. Equivalent to `-W0`.
$VERBOSE=nil

# Level 1: Enabled (medium). Equivalent to `-W1`.
$VERBOSE=false

# Level 2: Enabled (full). Equivalent to `-W2` or `-w`.
$VERBOSE=true

# Deprecation warnings are enabled.
Warning[:deprecated] = true

# Deprecation warnings are disabled.
Warning[:deprecated] = false

# Experimental warnings are enabled.
Warning[:experimental] = true

# Experimental warnings are disabled.
Warning[:experimental] = true

# Warnings are fully enabled including deprecations.
ENV["RUBYOPT"] = "-w -W:deprecated"

One nice aspect of being able to change warnings levels within your code — despite global mutation of objects — is you can disable experimental warnings, for example, when working with experimental features like Pattern Matching before they were fully supported or Ractors which are not fully supported yet. This allows you to silence warnings you are aware of but don’t need to be constantly reminded of. With great power comes great responsibility so don’t forget to reenable these warnings when done experimenting so you aren’t blind to new information and changes within the Ruby community at large.

IRB

IRB supports a subset of the same flags as ruby. Mainly, these flags: -w and -W[level]. There is no support for deprecation or experimental warnings, though.

Here’s a few examples but feel free to experiment further:

# Warnings enabled.
irb -w

# Warnings (Level 0)
irb -W0

Dependencies

One aspect of working with Ruby warnings always enabled is that you’ll be exposed to bad actors within the Ruby ecosystem who don’t fix their own warnings in a timely manner. First, you always want to report these issues and, if you have time, fix them yourself by opening up a code review. This can sometimes be time consuming so a workaround that allows you to work without constant noise from your dependencies is to silence all warnings except those within your project. Thankfully, this can be easily addressed by installing the Warning gem.

At a minimum, you’ll want to add the Warning gem to your test environment like so:

group :test do
  gem "warning", "~> 1.3"
end

Then you can configure your test environment to ignore all warnings from your gem dependencies. For example, this is what I add to my RSpec spec_helper.rb:

require "warning"
Gem.path.each { |path| Warning.ignore(//, path) }

That’s it! The above ensures, by using an empty regular expression, that all gem related warnings based on their path is ignored. Now you can focus on and resolve only the warnings related to your own Ruby project. There is a lot more the Warning gem can do for you so make sure to check out the documentation.

As hinted at earlier, try not to make this a permanent fixture by keeping tabs on upstream dependencies to see if they’ve fixed their warnings so you can remove this workaround.

RSpec

As mentioned earlier, a nice RSpec feature is that you can configure your entire test suite to have warnings enabled by default. This is also why I don’t use the -w flag globally via RUBYOPT but keep warnings relative to projects I’m working on since the actionable impact is so much more valuable. I highly recommend doing this since it’s one of the easiest and best ways to stay on top of upstream changes. Here’s the one liner that you’ll want to add:

RSpec.configure do |config|
  config.warnings = true
end

💡 If you’d like a more in-depth look at how to configure RSpec then I’d suggest you take a look at my RSpec Antipatterns article where I detail the entire configuration.

Conclusion

As you can see, there is a lot you can do with Ruby warnings so I hope you’ve learned a few more tricks to add to your Ruby toolbox and enabled warnings so you can stay on top of the constantly changing Ruby landscape. Enjoy!