Reason #81 • March 22nd, 2026

Combining hashes with Hash#merge

There are many situations where it can be useful to combine hashes together. For example, to apply default values to a hash of options, or to combine two sets of configuration together. Ruby provides the Hash#merge(other_hash) method for this purpose, which returns a new hash containing the contents of both hashes, with the right-hand side hash taking precedence in case of key conflicts:

Ruby
require "json"
require "net/http"

class JsonHttpClient
  DEFAULT_HEADERS = {
    "Content-Type" => "application/json",
    "Accept" => "application/json"
  }.freeze

  def initialize(tenant_id, api_key)
    @tenant_id = tenant_id
    @api_key = api_key
  end

  def post(url, params = {})
    headers = DEFAULT_HEADERS.merge(
      "Authorization" => "Bearer #{@api_key}"
    )
    tenant_scoped_params = params.merge(tenant_id: @tenant_id)

    Net::HTTP.post(URI(url), tenant_scoped_params.to_json, headers)
  end
end

client = JsonHttpClient.new("tenant_123", "secret_api_key")
client.post("https://api.example.com/people", { name: "David" })
      
JavaScript
class JsonHttpClient {
  static DEFAULT_HEADERS = {
    "Content-Type": "application/json",
    Accept: "application/json",
  };

  constructor(tenantId, apiKey) {
    this.tenantId = tenantId;
    this.apiKey = apiKey;
  }

  post(url, params = {}) {
    const headers = {
      ...JsonHttpClient.DEFAULT_HEADERS,
      Authorization: `Bearer ${this.apiKey}`,
    };

    const tenantScopedParams = {
      ...params,
      tenant_id: this.tenantId,
    };

    return fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(tenantScopedParams),
    });
  }
}

const client = new JsonHttpClient("tenant_123", "secret_api_key");
client.post("https://api.example.com/people", { name: "David" });
      

In case we need to resolve conflicts in a more complex way than "right-hand side wins", merge also accepts a block that will be called for each conflicting key, with the key and both values passed in as arguments. The block should return the value to use for that key in the resulting hash:

Ruby
base_config = {
  timeout: 5,
  retries: 2,
  tags: ["base"]
}
slow_request_config = {
  timeout: 60,
  tags: ["slow"]
}

base_config.merge(slow_request_config) do |_key, old_value, new_value|
  if old_value.is_a?(Array) && new_value.is_a?(Array)
    old_value + new_value
  else
    new_value
  end
end
# => { timeout: 60, retries: 2, tags: ["base", "slow"] }
      
JavaScript
const baseConfig = {
  timeout: 5,
  retries: 2,
  tags: ["base"],
};

const slowRequestConfig = {
  timeout: 60,
  tags: ["slow"],
};

const merged = Object.entries(slowRequestConfig).reduce(
  (acc, [key, value]) => ({
    ...acc,
    [key]:
      Array.isArray(acc[key]) && Array.isArray(value)
        ? [...acc[key], ...value]
        : value,
  }),
  { ...baseConfig }
);
// => { timeout: 60, retries: 2, tags: ["base", "slow"] }
      

As with many similar methods in Ruby, there is also a bang version merge! that mutates the original hash instead of returning a new one. This is useful when dealing with large hashes where performance is a concern, or when we explicitly want to mutate the original hash for some reason.

History

Hash#merge was added in Ruby 1.8, released in 2003. However, before that, Ruby had a Hash#update method that provided similar functionality for merging hashes in place (mutating the original hash). The original implementation of merge essentially just duplicated the hash and then called update on it.

In Perl, it is commonplace to merge hashes using the %merged = (%hash1, %hash2) syntax, which may have inspired Ruby's merge functionality, though there is no obvious single method in Perl that corresponds directly to Hash#merge.

Reason #82 ?