skip to content

In Ruby, what does `def send_email(to:, subject: "(no subject)", cc: [])` declare, and what happens when a caller omits `to:` or adds `bcc:`?

level: juniorimportance: must knowfreq 72%

answer

  1. trailing colon without a value
  2. any order at the call site
  3. default expression runs per call
  4. missing keyword: :to
  5. unknown keyword: :bcc

basics

~20 s

The signature declares one required keyword, to:, and two optional keywords, subject: and cc:, that fall back to their defaults. Omitting to: raises ArgumentError (missing keyword: :to); an unlisted bcc: raises ArgumentError (unknown keyword: :bcc).

solid answer

~40 s

A keyword parameter written as `to:` with nothing after the colon is **required**; `subject: "(no subject)"` and `cc: []` are **optional** and take their default when the caller leaves them out. Callers pass keywords by name, in any order, after any positional arguments: `send_email(cc: ["[email protected]"], to: "[email protected]")`. The default expression is evaluated on each call that omits the keyword, so every such call gets its own fresh `[]`. Ruby checks the names strictly before the body runs: a missing required keyword raises `ArgumentError` with `missing keyword: :to`, and a keyword the method does not list raises `ArgumentError` with `unknown keyword: :bcc`. Only a `**` parameter in the signature would collect such extras instead of rejecting them.

code

ruby · 20 lines
ruby
def send_email(to:, subject: "(no subject)", cc: [])
  "#{to} | #{subject} | cc=#{cc.size}"
end

send_email(to: "[email protected]")
# => "[email protected] | (no subject) | cc=0"
send_email(cc: ["[email protected]"], subject: "Hi", to: "[email protected]")
# => "[email protected] | Hi | cc=1"

begin
  send_email(subject: "Hi")
rescue ArgumentError => e
  e.message # => "missing keyword: :to"
end

begin
  send_email(to: "[email protected]", bcc: "[email protected]")
rescue ArgumentError => e
  e.message # => "unknown keyword: :bcc"
end

go deeper

for a junior

Recall the two forms: a name with a bare trailing colon is required, a name with a value after the colon is optional. Know both ArgumentError messages, missing keyword and unknown keyword.

for a middle

Explain that optional defaults are evaluated on each call, can use parameters to their left, and that strict name checking turns caller typos into errors unless a ** parameter is present.

for a senior

Show how required keywords make an email API fail fast at the call site, and argue when accepting a ** catch-all is worth losing typo detection.

for a principal

Weigh ever-growing keyword signatures against grouping settings into a dedicated value object, and how each choice ripples through callers in a large codebase.

## What the signature declares A **keyword parameter** is a method parameter that the caller fills by name rather than by position. In a `def`, it is written as a name followed by a colon. What comes after the colon decides whether the caller must supply it: | Form in the `def` | Kind | If the caller omits it | |---|---|---| | `to:` | required keyword | `ArgumentError`: `missing keyword: :to` | | `subject: "(no subject)"` | optional keyword | the default string is used | | `cc: []` | optional keyword | a new empty Array is used | So `def send_email(to:, subject: "(no subject)", cc: [])` accepts exactly three names. Nothing else is allowed unless the method also declares a keyword splat (`**opts`), which gathers extra names into a Hash. ## How callers pass keywords - Keywords are written as `name: value` pairs in the call: `send_email(to: "[email protected]", subject: "Invoice")`. - **Order does not matter.** `send_email(subject: "Invoice", to: "[email protected]")` binds the same values. - Keywords come **after** any positional arguments. A method can mix both: `def send_email(template, to:, cc: [])` is called as `send_email(:welcome, to: "[email protected]")`. - The names are **Symbols** matched exactly. `To:` or `"to"` is a different name and does not fill `to:`. - A keyword's value can be any expression, such as `send_email(to: user.email, cc: team.map(&:email))`; it is evaluated by the caller before the call, like any other argument. ## When the defaults are evaluated Ruby binds arguments in a fixed sequence each time the method is called: 1. It matches every keyword the caller passed against the declared names. 2. For each optional keyword the caller left out, it **evaluates the default expression now**, left to right. 3. Only then does the method body start. Two consequences follow. First, `cc: []` builds a **new Array on every call** that omits `cc:`, so appending to `cc` inside the method never leaks into the next call. Second, a default may refer to parameters declared **to its left**, which is handy for derived settings: ```ruby def send_email(to:, reply_to: to, subject: "(no subject)") [to, reply_to, subject] end send_email(to: "[email protected]") # => ["[email protected]", "[email protected]", "(no subject)"] ``` A default can also call a method on `self`, such as `from: default_sender`, because it runs inside the method's own context. ## The two errors and their exact messages Both failures are `ArgumentError`, raised while the arguments are bound, so the body never runs with a half-filled signature. - **Missing required keyword.** `send_email(subject: "Hi")` raises `missing keyword: :to`. With several absent, Ruby lists them all in one plural message: `missing keywords: :to, :from`. - **Unknown keyword.** `send_email(to: "[email protected]", bcc: "[email protected]")` raises `unknown keyword: :bcc`. Several unknown names are reported together, `unknown keywords: :bcc, :priority`. - The names appear in their `inspect` form, which is why each keeps its leading colon. Because the check happens during binding, the error is raised at the caller's line, where the fix belongs, and not somewhere deep in the delivery code. This strictness is a feature. A typo such as `subjcet:` fails loudly at the call site instead of being silently dropped, and a forgotten recipient cannot reach the delivery code as `nil`. ## What does not happen Several beliefs carried over from other code styles are wrong for Ruby keywords: - An omitted required keyword is **not** set to `nil`; the call fails. - An unknown keyword is **not** ignored; without `**opts` it is an error. - A missing keyword is **not** a `KeyError`. That class belongs to lookups such as `Hash#fetch`, not to argument binding. - The default `[]` is **not** shared between calls, because the expression is re-evaluated on each call that needs it. ## Why interviewers ask it The question checks whether a candidate reads a signature precisely: which parameters are mandatory, which have fallbacks, and what the method will reject. For an email-sending API, required keywords make the essentials (`to:`) impossible to forget, optional keywords document every setting with its default in one line, and strict name checking catches misspelled options the moment a caller writes them. The next step up, collecting extra settings with `**opts` or refusing keywords with `**nil`, trades part of that strictness for flexibility.

  • Can a keyword's default refer to another parameter, as in `reply_to: to`?
    Yes, as long as that parameter is declared to its left. Defaults are evaluated left to right at call time, after the earlier parameters are bound, so `def send_email(to:, reply_to: to)` gives `reply_to` the value of `to` whenever the caller omits it. A default may also call a method on `self`, such as `from: default_sender`.
  • What does Ruby report when two required keywords are missing at once?
    One `ArgumentError` that names all of them: `missing keywords: :to, :from`. The word becomes plural and each name is shown in `inspect` form. Unknown keywords are grouped the same way, `unknown keywords: :bcc, :priority`, so a caller sees every misnamed argument in a single failure rather than one per retry.

saying these in an interview costs you the question

  • An omitted required keyword is simply nil inside the method.
  • Keyword arguments must be passed in the order the def lists them.
  • Unknown keywords are ignored unless the method checks for them itself.
  • The cc: [] default is built once and shared by every call.
  • A missing keyword raises KeyError, just like Hash#fetch.