skip to content

A timing wrapper takes a remote-lookup function and returns a new function — why must the returned function keep the original's signature?

level: juniorimportance: must knowfreq 65%

answer

  1. same shape in, same shape out
  2. one wiring line, no call sites
  3. parameters, return value, failure path
  4. uniform shape is what lets wrappers stack
  5. break the shape, break every caller

basics

~20 s

Keeping the same parameters and return type makes the wrapper a drop-in: every existing call site keeps working, the wrapper goes in and comes out at one wiring line, and other wrappers can stack around it.

solid answer

~40 s

A wrapper is a function that takes a function and returns one that calls it with extra behaviour around the call — timing, retry, caching, logging. What makes that cheap is that the returned function has the same shape as the one it replaces: same parameters, same kind of return value, failures propagating the same way. Because of that you install it at the single place the function is wired up, and no call site changes. It also means wrappers stack: anything that accepts a `lookup(key)` function accepts `withTiming(lookup)`, so a second wrapper can wrap the first without knowing what it is. Break the shape — add a parameter, return a different type, hide an error — and the wrapper stops being a wrapper and becomes a rewrite of every caller.

code

pseudocode · 8 lines
pseudocode
function withTiming(inner)
    return function(key)
        start = clockNow()
        result = inner(key)
        record("lookup_ms", clockNow() - start)
        return result

lookup = withTiming(lookup)   // one wiring line; callers unchanged

go deeper

for a junior

Be able to say the shape out loud: a function goes in, a function of the same shape comes out, and the extra behaviour happens around the inner call. Then say why callers never change.

for a middle

Explain how the returned function forwards arguments and results untouched, where the wrapper's own state lives, and what a variable-length argument list buys and costs when the shape is unknown.

for a senior

Show that you install wrappers at the composition point rather than scattering them, and that you treat the failure path as part of the shape — a wrapper that changes error behaviour is a behaviour change, not an addition.

for a principal

The judgment is where wrapping belongs at all: which behaviours are worth standardising as wrappers everyone applies, and which are hidden magic that makes a stack trace unreadable for the next team.

## What a wrapper is A **wrapper** is a higher-order function: it takes a function as its argument and returns a **new function**, which callers then use in place of the original. Inside that returned function the wrapper does something before the inner call, something after it, or both — starts and stops a timer, counts an attempt, consults a store, writes a log line — and calls the function it was handed in the middle. Concretely: the thing being wrapped is a remote lookup that takes a key and returns a value. A timing wrapper takes that lookup and hands back **another key-to-value function**. Callers still write `lookup(key)`. The only difference is that a duration gets recorded somewhere on the way through. Nothing at the call site knows a wrapper exists, and that is the whole point. ## Why the returned function must keep the same shape **Same shape** means the same parameters in the same order, the same kind of return value, and the same behaviour when the inner call fails. Three things depend on it. 1. **No call site changes.** The wrapper is installed where the function is created or wired up — one line. Every place that already calls the function keeps compiling and keeps working. A wrapper that needs its callers edited has bought you nothing over editing the function itself. 2. **Wrappers stack.** Because the wrapped function has the same shape as the raw one, a second wrapper can wrap the first, and a third can wrap that. The stack is built out of one uniform part, which is what lets timing, retry and caching be written once each and combined in any order. 3. **It comes out as easily as it went in.** Wrapping is reversible: delete the wiring line and the original function is back. Behaviour you can add and remove at one line is behaviour you can turn off under load, leave out of a test, or enable for one environment. | If the wrapper changes… | What it costs | |---|---| | the parameter list | every call site must be edited; the wrapper has become a rewrite | | the kind of value returned | callers must unwrap or convert, so the wrapper leaks into their code | | the behaviour on failure | error handling written against the original stops firing | | nothing a caller can observe | it installs and uninstalls at a single line | ## Writing one - **Take the function as a value**, not a name you call directly — the wrapper must work for any function of that shape, not one specific function. - **Forward every argument unchanged.** Where the argument list is fixed and known, mirror it exactly. Where it is not, accept a variable-length list and pass it straight through; you gain generality and give up whatever checking the fixed signature provided. - **Return the inner result untouched** — not a summary of it, not a copy, not a status object. The caller asked for a value. - **Let failures through.** The wrapper may observe an error, count it, or call again after it; converting or hiding it changes what callers see. - **Keep the wrapper's own state inside the returned function's captured scope**, so two wrapped functions never share a timer or a counter by accident. Languages differ in how much ceremony the returned function needs — some let you write it inline as an expression, others want a named type for it first — but the mechanism is the same everywhere: a function in, a same-shaped function out. ## What this buys you The reason interviewers reach for this leaf is that the long-hand arrangement collapses. Adding timing to five functions without a wrapper means five copies of the timing code inside five unrelated bodies, each of which now does two jobs. With a wrapper there is one `withTiming`, it serves every function of that shape, and the five functions go back to doing only their own work. The extra behaviour lives in one place and is applied at the edges. ## Where this goes wrong for beginners - **Adding a switch parameter** to the original function instead of wrapping it — every caller now has to pass it, and the function has grown a second job. - **Returning the inner function instead of calling it.** Handing back `inner` rather than a new function that calls `inner` compiles fine and silently does nothing extra. - **Hiding failures** so the wrapper can always record a tidy result, which quietly deletes the caller's error handling. - **Recording the wrong thing** because the wrapper's work happens after a branch that did not run.

  • How do you write a wrapper for a function whose argument list you do not know in advance?
    Have the returned function accept a variable-length argument list and forward it unchanged to the inner function, returning whatever comes back. The wrapper then works for any arity. The cost is that you give up the checking a fixed signature gave you, so a wrapper written for one known shape stays safer where that shape is known.
  • Is a wrapper allowed to change what happens when the inner function fails?
    Not if it is meant to be invisible. A caller that handled a failure before must still see that failure. A wrapper may observe an error, count it, log it, or call again after it — but converting it into a normal return value, or hiding it, changes how every caller behaves and the wrapper is no longer a drop-in.
  • What does introducing the wrapper let you delete?
    The copies of the extra behaviour that would otherwise sit inside each function body. One wrapper serves every function of that shape, so timing or logging code exists once instead of once per implementation, and the wrapped functions go back to expressing only their own job.

A wrapper is a travel adapter with the same plug shape on both sides: everything that fitted before still fits, and you can push a second adapter onto the first.

saying these in an interview costs you the question

  • Thinks the wrapper must add a parameter to the function to switch the behaviour on
  • Edits each call site to opt into the wrapper instead of wrapping at the wiring point
  • Hides errors from the inner function so the wrapper can always record a result
  • Returns the inner function itself rather than a new function that calls it
  • Cannot say what the wrapper returns, only that it 'adds logging'