Ruby 2.6

  • Released at: Dec 25, 2018 (NEWS file)
  • Status (as of Feb 07, 2023): EOL, latest is 2.6.10
  • This document first published: Dec 29, 2018
  • Last change to this document: Feb 07, 2023

Note: As already explained in Introduction, this site is dedicated to changes in the language, not the implementation, therefore the list below lacks mentions of lots of important optimization introduced in 2.6, including the whole JIT big thing. That’s not because they are not important, just because this site’s goals are different.



Endless range: (1..)

  • Discussion: Feature #12912
  • Reason/Usage: More convenient Array slicing, and idiomatic open-ended case conditions
  • Documentation: Range: Endless ranges
  • Code:
    # Usage
    ary = %w[List of words]
    ary[2..] # => ["words"]
    case indicator
    when 1...8 then # ...
    when 8...15 then # ...
    when 15.. then # ...
    # Details of behavior:
    (1..).end         # => nil
    (1..).to_a        # RangeError (cannot convert endless range to an array)
    (1..).each { }    # hangs forever
    (1..) == (1...)   # => false
    (1..).size        # => Infinity
  • Follow-up: “Beginless” range was introduced in 2.7.

Non-ASCII constant names

Constant names may start with a non-ASCII capital letter.

else in exception-handling context

In exception-handling context, else without any rescue is now a syntax error

  • Discussion: Feature #14606
  • Code:
    # In Ruby 2.6 it would be SyntaxError: else without rescue is useless
    [1,2,3].each do
      p :foo
      p :bar

    The code above obviously contains some error (omitted if or something), but before Ruby 2.6 the interpreter would not complain, interpreting it as “begin → perform code that might raise → else (if nothing was raised) performing something”, due to lesser-used Ruby feature of allowing else in an exception-handling construct.

Refinements: improved visibility

Refinements are now compatible with #public_send and #respond_to?, and implicit #to_proc.

  • Reason: This is part of the effort to make great yet neglected feature of refinements behave more naturally;
  • Discussions: Feature #14223, Feature #15326, Feature #15327
  • Code:
    module StringExt
      refine String do
        def surround(before, after = before)
          after + self + before
        def to_proc
          proc { |val| self % val }
    using StringExt
    'foo'.respond_to?(:surround) # => true in 2.6, false in 2.5
    'foo'.public_send(:surround, '|') # => "|foo|" in 2.6, NoMethodError in 2.5
    (1..3).map(&'%02i') # => ["01", "02", "03"] in 2.6;  wrong argument type String (expected Proc) in 2.5
  • Follow-up: Ruby 2.7 also made refinements available in #method


  • Infamous esoteric flip-flop syntax is deprecated finally: Feature #5400.
    • Follow-up: Deprecation is reverted in 2.7

Core classes and modules


Notice that methods defined in Kernel are typically available on any object, that’s why these changes are important.

#then as an alias for #yield_self

  • Reason: Since the introduction of Kernel#yield_self at Ruby 2.5, it was pointed out that the name chosen is too long for this basic method, and, unlike most of other core methods, says “how it is implemented” not the intention; after lots of discussion it was decided #then corresponds best to the method’s goal.
  • Notice: There is a controversy in the community about this alias, pointing out the fact that #then is a typical method name for promises.
  • Discussion: Feature #14594
  • Code:
    [BASE_URL, path].join('/')
        .then { |url| open(url).read }
        .then { |body| JSON.parse(body, symbolyze_names: true) }
        .dig(:data, :items)
        .then { |items| File.write('response.yml', items.to_yaml) }

<Numeric>() methods have exception: argument

  • Reason: As of Ruby 2.5, Integer('x') will raise ArgumentError (invalid value for Integer(): x), but in a lot of cases a sane thing to desire is “convert it, if possible”
  • Discussion: Feature #12732
  • Affected methods: #Integer, #Float, #Rational, #Complex and #BigDecimal (stdlib; previously known as
  • Documentation: Kernel#Integer, Kernel#Float, Kernel#Rational, Kernel#Complex, Kernel#BigDecimal
  • Code:
    Integer('x') # => ArgumentError (invalid value for Integer(): "x")
    Integer('x', exception: false) # => nil

#system has exception: argument

With exception: true, the method raises instead of returning false on non-0 exit code, or nil on command execution failure.

  • Discussion: Feature #14386
  • Documentation: Kernel#system (unfortunately, seems documentation haven’t been updated with new feature)
  • Code:
    system('cat nonexistent.txt')  # => false
    system('ctat nonexistent.txt') # => nil
    system('cat nonexistent.txt', exception: true)  # RuntimeError (Command failed with exit 1: cat)
    system('ctat nonexistent.txt', exception: true) # Errno::ENOENT (No such file or directory - ctat)

Module#method_defined?: inherit argument

Module#method_defined? and similar methods accept an optional second argument. If it is false, only module’s own methods would be returned.

  • Reason: Other module introspection methods, like #methods, already have similar arguments. It may be important for meta-programming; like “when this module is included, it will redefine some host’s methods, but only those that belong to host”, or “test that all descendants of some abstract class redefine required methods”.
  • Affected methods: #method_defined?, #private_method_defined?, #protected_method_defined?, #public_method_defined?.
  • Discussion: Feature #14944
  • Documentation: Module#method_defined?, Module#private_method_defined?, Module#protected_method_defined?, Module#public_method_defined?
  • Code:
    # => true
    Array.method_defined?(:chunk, false)
    # => false -- it is Enumerable's method
    Array.method_defined?(:to_h, false)
    # => true  -- despite inheriting from Enumerable, Array redefines it for performance
  • Follow-up: In 2.7, inherit argument was also added to autoload?

String#split with block

  • Reason: When parsing a huge string, and each substring is used exactly once, it could be ineffective to first create the whole array of parts, and only then iterate through it; with block form, parts are just yielded one by one;
  • Discussion: Feature #4780
  • Documentation: String#split
  • Code:
    "several\nlong\nlines".split("\n") { |part| puts part if part.start_with?('l') }
    # prints:
    #   long
    #   lines
    # => "several\nlong\nlines"

    Note that in block form, the original string will be returned, not the result of processing parts with block. To work with split results in a method-chaining style, one can utilize Object#to_enum:

      .to_enum(:split, "\n")  # => Makes a enumerator, yielding each entry from split("\n")
      .each_with_object( { |ln, h| h[ln.length] += 1 }
      # => {7=>1, 4=>1, 5=>1}

Time: support for timezones

The concept of a “timezone object” is introduced for various Time methods. Ruby does not define any timezone classes by itself, but the API expected corresponds to that of TZInfo::Timezone:

A timezone argument must have local_to_utc and utc_to_local methods, and may have name and abbr methods.

  • Methods affected:
    • .new (timezone may be passed as a last argument, which previously accepted only raw UTC offsets)
    • .at (new keyword in: argument)
    • #getlocal (accepts timezone where previously only UTC offset was accepted)
    • #+, #-, #succ (no new argument, but preserve timezone of the source)
  • Discussion: Feature #14850
  • Documentation: Time: Timezone argument
  • Reason: Named timezones are more complicated than just “offset from UTC”. Going over DST date, or between different years in country history, time could have the same timezone, but different UTC offset.
  • Code:
    zone = TZInfo::Timezone.get('America/New_York')
    time =, 6, 1, 0, 0, 0, zone)                 # => #<TZInfo::DataTimezone: America/New_York>
    time.strftime('%H:%M %Z') # => "00:00 EDT"
    time.utc_offset           # => -14400 = -4 hours
    time += 180 * 24*60*60    # + 180 days, summery->winter transition
    time.utc_offset           # => -18000, -5 hours -- daylight saving handled by timezone
  • Follow-up: In Ruby 3.1, the new ways for handier constructing time with timezones were introduced.

Proc composition

Proc and Method classes now have #>> and #<< methods for functional composition.

  • Reason: This was a long-anticipated feature for moving Ruby towards more functional code.
  • Discussion: Feature #6284
  • Code:
    plus = ->(x, y) { x + y }
    mul2 = ->(x) { x * 2 }
    stringify = :to_s.to_proc
    (plus >> mul2).call(5, 6)
    # => 22
    (mul2 >> stringify).call(5) # (5 * 2).to_s
    # => "10"
    (mul2 << stringify).call(5) # 5.to_s * 2
    # => "55"
    # Realistic examples:
    # 1. Providing chain of functions instead of chaining map's: >> :body.to_proc >> JSON.method(:parse) >> ->(data) { data.dig('response', 'items')})
    # 2. Storing chain of processings in constant:
    RESPONSE_PROCESSOR = Faraday.method(:get) >>
                        :body.to_proc >>
                        JSON.method(:parse) >>
                        ->(data) { data.dig('response', 'items')}
    # ...later...
    # 3. Utilizing block-based DSLs (Sinatra-alike)
    get '/my_endpoint', &parse_params >> perform_business_action >> render_response
  • Important notice: Unlike any other places in Ruby (where objects are coerced into procs with #to_proc method), “what can be chained” is decided by existence of #call method; this means you CAN’T chain symbols (notice :body.to_proc above), but CAN chain some “command pattern” classes with API.

Array#union and Array#difference

Array#union is like |, but also accepts multiple arguments; Array#difference is like -, but accepts multiple arguments.

  • Discussion: Feature #14097
  • Reason: Multiple-argument methods are more effective, and better chainable than operator form
  • Documentation: Array#union, Array#difference
  • Code:
    [1, 2, 3].union([3, 4], [4, 5])
    # => [1, 2, 3, 4, 5]
    [1, 2, 3, 4].difference([3, 4], [1])
    # => [2]
  • Notice: There are also plans (discussed in the same ticket above) to introduce mutating union! form.
  • Follow-up: Ruby 2.7 added #intersection method.

Hash#merge with multiple arguments

  • Discussion: Feature #15111
  • Methods affected: Hash#merge, Hash#merge!, Hash#update (alias for #merge!)
  • Documentation: Hash#merge
  • Code:
    {a: 1, b: 2}.merge({b: 3, c: 4}, {c: 5, b: 6}) # => {a: 1, b: 6, c: 5}
    {a: 1, b: 2}.merge({b: 3, c: 4}, {c: 5, b: 6}) { |key, oldval, newval| [oldval, newval] }
    # => {a: 1, b: [[2, 3], 6], c: [4, 5]}

    Note the last example: if the block is provided for conflict resolution, it is called repeatedly for each pair of values provided for the conflicting key (not |key, *all_conflicting_values| as one may expect).



#select is aliased as #filter (and #select! as #filter!, where applicable).

  • Reason: It was argued for a long time that most of other languages name the concept “filter”, so the new alias was added to lessen the confusion for newcomers.
  • Discussion: Feature #13784
  • Classes and modules affected: Enumerable, Enumerator, Enumerator::Lazy, Struct (only #filter), Array, Hash, and Set of standard library (#filter and #filter!).

#to_h with a block

  • Reason: It was noted that .map { |...| [some_key, some_value] }.to_h is a very common pattern and should be made DRY;
  • Discussion: Feature #15143;
  • Classes and modules affected: Enumerable and everything that includes it;
  • Documentation: Enumerable#to_h, Array#to_h, ENV.to_h, Hash#to_h, Struct#to_h
  • Code:
    {a: 1, b: 2, c: 3}.to_h { |k, v| [k.to_s, -v] } # => {'a' => -1, 'b' => -2, 'c' => -3}
    File.readlines('test.txt').each_with_index.to_h { |l, i| [i, l] }
    # => {0 => 'first line', 1 => 'second line', 2 => 'third line'}


Range#step and Numeric#step are now returning not an instance of Enumerator, but Enumerator::ArithmeticSequence instead.

  • Reason/Usage: This feature was added by request of scientific Ruby community; it is useful for reusing results of ( as a value object for idiomatic slicing of custom collections, see also Range#%.
  • Discussion: Feature #13904
  • Documentation: Enumerator::ArithmeticSequence, Range#step, Numeric#step
  • Code:
    # Basic usage remains the same:
    # => [1, 3, 5, 7, 9]
    enum = (1..10).step(2)
    # => ((1..10).step(2))
    # => Enumerator::ArithmeticSequence
    # => [Enumerator::ArithmeticSequence, Enumerator, Enumerable ...]
    # So, it is just a specialized subclass of enumerator, adding this methods:
    enum.begin  # => 1
    enum.end    # => 10
    enum.step   # => 2
  • Follow-up: In 3.0, Array slicing with ArithmeticSequence (from 1st to 10th, each 2nd element) became possible.

Enumerator chaining

Several enumerators can now be chained into one with Enumerator#+(other) or Enumerable#chain(list, of, enumerators). The result of the operation is Enumerator::Chain (specialized subclass of Enumerator).

  • Reason: Cleaner expression of chaining enumeration for several sequences, especially for lazy enumerators
  • Discussion: Feature #15144
  • Documentation: Enumerator#+, Enumerable#chain, Enumerator::Chain
  • Code:
    [1, 2, 3].chain
    # => #<Enumerator::Chain: [[1, 2, 3]]>
    [1, 2, 3].each + [4, 5, 6].each
    # => #<Enumerator::Chain: [#<Enumerator: [1, 2, 3]:each>, #<Enumerator: [4, 5, 6]:each>]>
    [1, 2, 3].chain([4, 5, 6])
    # => #<Enumerator::Chain: [[1, 2, 3], [4, 5, 6]]>
    # Realistic use-case:
    # Take data from several sources, abstracted into enumerator, fetching it on demand
    sources = { |url| open(url).read }
      .chain( { |path| })
    # ...then uniformely search several sources (lazy-loading them) for some value
    sources.detect { |body| body.include?('Ruby 2.6') }


Range#=== uses #cover? instead of #include?

  • Reason: Previously, case equality operator used #include?, which underneath iterates through entire range (except for Numerics). With objects other than numbers it could be ineffective (creating thousands of objects), impossible (if there is no notion of “next object”, but exists notion of order) or imprecise.
  • Discussion: Feature #14575
  • Code:
    when + 1
      # this would've not been reached before Ruby 2.6
    # this would raise "can't iterate from Gem::Version" before Ruby 2.6
    gem 'ruby-ip'
    require 'ip'
    # this would perform ~0.5 sec before Ruby 2.6, iterating over 65536-elt sequence
  • Notice: For String ranges, behavior left unchanged, so example with versions above would NOT work with pure-String versions.
  • Follow-up: String behavior was fixed in Ruby 2.7, so in 2.7 this code prints “yes”:
    case '2.5'
    when '1.8.7'..'2.6'
      puts "yes"
      puts "no"

Range#cover? accepts range argument

Range#% alias

  • Reason/Usage: It was proposed to have short syntax of complex array slicing, usable for math algorithms; see also Enumerable::ArithmeticSequence.
  • Discussion: Feature #14697
  • Code:
    (5..20) % 2
    # => ((5..20).%(2)) -- an instance of Enumerable::ArithmeticSequencee
    some_fancy_collection[(5..20) % 2] # each second element in 5..20 range

    Note that () around the range is mandatory here, because 5..20 % 2 will be parsed as (5)..(20 % 2)

  • Notice: Ruby’s Array doesn’t support slicing with Enumerable::ArithmeticSequencee as of 2.6.
  • Follow-up: In 3.0, Array slicing with ArithmeticSequence became possible.


New arguments: receiver: and key:

NameError, NoMethodError accept :receiver on creation; KeyError accepts :receiver and :key.

  • Reason: Since Ruby 2.5, these exception classes were “introspectable”: when you catch them, you can fetch the object that caused the problem and (in case of KeyError) problematic key; but there were no way to add that helpful data when raising an exception in your own code.
  • Discussion: Feature #14313
  • Documentation:, (docs not updated),
  • Code:
    class MyFancyCollection
      def [](key)
        # ...
        raise"don't have this: #{key}", receiver: self, key: key)
        # ...
  • Notice: <Exception>.new syntax is the only way to pass new arguments, this would not work:
    raise KeyError, "don't have this: #{key}", receiver: self, key: key
  • Follow-up: Ruby 2.7 adds receiver: argument for FrozenError, too.

Exception#full_message options

Exception#full_message (which returns “exceptions how they are printed by Ruby”, including message, class and backtrace) takes :highlight and :order options. This means client code can fine-tune which result it wants to achieve.

  • Reason: Since introduction of new way for printing exceptions to STDERR (backtrace in reverse order and message is highlighted in bold), there were some improvements proposed to handle different contexts of exception printing.
  • Discussion: Bug #14324
  • Documentation: Exception#full_message
  • Code:
      {a: 1}.fetch(:b)
    rescue => e
      # highlight: true/false, order: :top/:bottom
      p e.full_message(highlight: false, order: :top)
      # "t.rb:2:in `fetch': key not found: :b (KeyError)\n\tfrom t.rb:2:in `<main>'\n"
      p e.full_message(highlight: true, order: :bottom)
      # "\e[1mTraceback\e[m (most recent call last):\n\t1: from t.rb:2:in `<main>'\nt.rb:2:in `fetch': \e[1mkey not found: :b (\e[1;4mKeyError\e[m\e[1m)\e[m\n"
      p e.full_message
      # Output could be either of two above, depending on whether STDERR is terminal
      # or redirected with `ruby test.rb 2> err.log
  • Follow-up: In Ruby 3.2, one more related method #detailed_message was added.

Exception output tweaking

  • Exception#cause is now printed if STDERR is the terminal:
      {a: 1}.fetch(:b)
    rescue => e
      raise 'something went wrong'
    # Traceback (most recent call last):
    #   1: from t.rb:2:in `<main>'
    # t.rb:2:in `fetch': key not found: :b (KeyError)
    #   1: from t.rb:1:in `<main>'
    # t.rb:4:in `rescue in <main>': something went wrong (RuntimeError)

    Notice the exception that led to rescue/raise block (KeyError) is printed too.

  • Exception backtrace and error message are printed in reverse order when the exception is not caught and STDOUT is unchanged and a tty. Feature #8661

Filesystem and IO

Dir#each_child and Dir#children

  • Discussion: Feature #13969
  • Documentation: Dir#each_child, Dir#children
  • Code:'.').children
    # => ["_layouts", ".gitignore", "", "_data", "_src", "_site", "_config.yml", "", "js", "404.html", ".git", "images", "Gemfile", "css", "Gemfile.lock"]

IO open mode: 'x'

When opening a new file for writing, 'wx' can be specified to request the file does not exist before opening.

  • Discussion: Feature #11258
  • Documentation: IO: Open mode
  • Code:
    f1 ='temp.txt', 'wx')
    # => #<File:temp.txt>
    f2 ='temp.txt', 'wx') # it is already created by previous statement
    # Errno::EEXIST (File exists @ rb_sysopen - temp.txt)

Minor changes

  • Object#=~ is deprecated: Feature #15231 (but NilClass#=~ is still allowed without deprecation notice).
  • Random.bytes: Feature #4938 / Random.bytes
    # => "8\a\xB0\xD1V"
    # ...will probably return different value for you, though :)



  • Reason: Important usage for this new feature is proper reporting in block-based DSL usage: if something was inconsistent in the passed block, library code may use block.binding.source_location to report where exactly problematic code came from.
  • Discussion: Feature #14230
  • Documentation: Binding#source_location
  • Code:
    # => ["(irb)", 114]
    def my_dsl(&block)
      puts "Evaluating block from #{block.binding.source_location.join(':')}"
    my_dsl { ... } # Prints "Evaluating block from (irb):118"


For string name, returns what path require(name) will load (without actually loading it).

  • Reason: Static analysis of the program (finding the code it will use without really loading it); understanding where dependencies are coming from.
  • Discussion: Feature #15230
  • Documentation:
  • Code:
    # => [:rb, "<...>/ruby-2.6.0/lib/ruby/2.6.0/net/http.rb"]
    # => [:so, "<...>/ruby-2.6.0/lib/ruby/2.6.0/x86_64-linux/"]
    # LoadError (cannot load such file -- garbage)
    require 'net/http'
    # => [:rb, false]  -- for already loaded libraries, the path would not be returned
    # For gems:
    # LoadError (cannot load such file -- faraday)
    gem 'faraday' # without this, gems can not be deduced
    # => [:rb, "<...>/gems/faraday-0.15.4/lib/faraday.rb"]
  • Follow-up: In Ruby 2.7, resolve_feature_path was moved to a $LOAD_PATH singleton method, and its behavior for already loaded pathes was fixed to return path nevertheless.


The new module provides an “official” Ruby parser, replacing stdlib Ripper (and third-party parsers). The module is considered experimental, and its API can change in the future versions.

  • Documentation: RubyVM::AbstractSyntaxTree
  • Code:
    tree = RubyVM::AbstractSyntaxTree.parse('1+2')
    # => #<RubyVM::AbstractSyntaxTree::Node(SCOPE(0) 1:0, 1:3): >
    # => :SCOPE
    # => [[], nil, #<RubyVM::AbstractSyntaxTree::Node(OPCALL(36) 1:0, 1:3): >]
    # => :OPCALL
    # => [#<RubyVM::AbstractSyntaxTree::Node(LIT(59) 1:0, 1:1): >, :+, #<RubyVM::AbstractSyntaxTree::Node(ARRAY(42) 1:2, 1:3): >]
    # => [1]
  • Note: One may be excited about RubyVM::AbstractSyntaxTree.of(proc), but it doesn’t mean the “real” extraction of code from live Proc, rather a simple hack with trying to find proc’s source file and parse it.
  • Follow-up: In Ruby 3.2, new options were added for parse, allowing to:

TracePoint improvements

Ruby TracePoint API allows to observe any events that are happening during program evaluation. In 2.6, there were several improvements to allow better control on event handling and their introspection.


On :b_call and :c_call (block and method call events), TracePoint now provides #parameters method to fetch call params definitions.

  • Discussion: Feature #14694
  • Documentation: TracePoint#parameters
  • Code:
    t = { |tp| p [tp.event, tp.parameters] }
    [1, 2, 3].map { |x| x.to_s }
    # [:b_call, [[:opt, :x]]]
    # [:b_call, [[:opt, :x]]]
    # [:b_call, [[:opt, :x]]]
  • Notice: Format of return value is the same as for Method#parameters

:script_compiled event

Event fired when a new piece of Ruby code is loaded (through eval, require or load). #instruction_sequence on this event will return compiled RubyVM::InstructionSequence object, and #eval_script will return the text of the script sent to eval.

  • Discussion: Feature #15287
  • Documentation: TracePoint: Events (though new event seems to be omitted), TracePoint#instruction_sequence, TracePoint#eval_script
  • Code:
    t = { |tp| p [tp.event, tp.instruction_sequence, tp.eval_script] }
    eval('p 1')
    # [:script_compiled, <RubyVM::InstructionSequence:<main>@(eval):1>, "p 1"]
    require 'net/http'
    # [:script_compiled, <RubyVM::InstructionSequence:<top (required)>@.../ruby-2.6.0/lib/ruby/2.6.0/net/http.rb:0>, nil]
    # [:script_compiled, <RubyVM::InstructionSequence:<top (required)>@.../ruby-2.6.0/lib/ruby/2.6.0/net/protocol.rb:0>, nil]
    # [:script_compiled, <RubyVM::InstructionSequence:<top (required)>@.../ruby-2.6.0/lib/ruby/2.6.0/socket.rb:0>, nil]
    # .....

#enable: new params target: and target_line:

With new parameters provided, you can fine-tune for what methods or specific lines to catch events.

  • Reason: Less trace garbage and performance footprint on tracing in complex codebase, allowing very specific tracepoints to be enabled even in production.
  • Discussion: Feature #15289.
  • Documentation: TracePoint#enable (not much, though…)
  • Code:
    t = { |tp| p tp }
    def m1
      p 1
    def m2
      p 2
    t.enable(target: method(:m1))
    # prints #<TracePoint:line@test.rb:5 in `m1'>
    # prints nothing
  • Notice: In absence of docs, we can at least point at commit message:

    code should be consisted of InstructionSequence (iseq) (RubyVM::InstructionSequence.of(code) should not return nil). If code is a tree of iseq, TracePoint is enabled on all of iseqs in a tree.

  • Follow-ups:
    • In 2.7, the docs have emerged, and a new parameter named target_thread: was introduced. It was missing from the official NEWS-file and therefore missing from this changelog (which is a thing to be fixed!)
    • In 3.2, target_thread: began defaulting to the current thread with block form of enable.

Standard library

Large updated libraries

Libraries promoted to default gems project has a nice explanations of default and bundled gems concepts, as well as a list of currently gemified libraries.

“For the rest of us” this means libraries development extracted into separate GitHub repositories, and they are just packaged with main Ruby before release. It means you can do issue/PR to any of them independently, without going through more tough development process of the core Ruby.

Libraries extracted in 2.6: