Which evaluation strategy still fails when an argument the body never uses would error or never terminate?
answer
- an argument nobody reads can still bite
- when it runs decides what survives
- termination, not only speed
- fallback helpers need one argument spared
- strict runs before the body chooses
basics
~20 sCall-by-value fails. Strict evaluation reduces the argument before the body runs, so a failing or non-terminating expression takes the call down even though the body would never have used it. Both non-strict strategies return normally.
solid answer
~40 sUnder call-by-value the argument is evaluated before control enters the body, so whether the body needed it is irrelevant: a failing expression fails the call and a non-terminating one hangs it. Under call-by-name and call-by-need the expression is only evaluated on demand, and there is no demand, so the call completes. This is why non-strictness is a semantic property rather than an optimisation — it changes which calls complete, not just how fast they run. It is also why a fallback-style helper, whose second argument is reached only when the first is missing, behaves usefully under a non-strict strategy and wastefully or fatally under a strict one.
code
pseudocode · 10 linesfunction or_else(value, fallback)
if value is present
return value
return fallback
or_else(cached_entry, load_from_far_away())
// cached_entry present:
// strict -> load_from_far_away() already ran
// non-strict -> fallback is never demanded, never runsgo deeper
The takeaway is order: with strict evaluation the argument runs before the body has a chance to decide anything, so an argument the body would have ignored can still fail the call.
Explain the three columns separately: cost, failure and non-termination. Only the first is a performance story; the other two mean the same source text produces different outcomes under different strategies.
Demonstrate the limit in real terms. Deferring a risky argument moves the failure inside the callee, which changes where it can be handled and what diagnostic context it carries; that relocation needs to be a deliberate choice.
The design question is whether the language gives callees a way to spare an argument at all. Without one, fallback-style helpers cannot be written faithfully as ordinary functions and the shape has to be built into the language.
## Evaluation order decides whether the call survives An argument expression can do one of three unhelpful things: cost a lot, fail, or never terminate. Strictness decides which of those the caller is exposed to when the body would not have used the argument at all. Under **call-by-value**, every argument is reduced to a value before control enters the body. There is no opportunity for the body's logic to intervene, because the body has not started. So a failing argument fails the call, and a non-terminating one hangs it, regardless of whether the parameter is ever mentioned in the body. Under **call-by-name** and **call-by-need**, the expression runs only when the body demands the parameter. No demand, no evaluation, no failure. The call returns normally and the caller never learns that the argument would have been trouble. ## The fallback helper The shape where this bites is the helper that chooses between two things: ``` function or_else(value, fallback) if value is present return value return fallback or_else(cached_entry, load_from_far_away()) ``` Trace the branch that fires when `cached_entry` is present. The body returns on the first branch and never touches `fallback`. Under a non-strict strategy `load_from_far_away()` never runs — the helper does what its name promises. Under call-by-value it runs every time, before the helper is even entered; if it is merely slow, the cache has been made pointless, and if it fails when the far-away thing is unreachable, the call fails on exactly the path the cache was supposed to protect. ## Cost, failure and termination are three different columns | The unused argument would… | Call-by-value | A non-strict strategy | |---|---|---| | Cost a lot | Cost is paid | Cost is not paid | | Fail | The call fails | The call returns normally | | Never terminate | The call never returns | The call returns normally | The first row is a performance difference and nothing more. The second and third rows are **semantic** differences: the same program text produces a different outcome — an error instead of a result, a hang instead of a return — depending only on the strategy. That is why strictness is described as part of a language's meaning rather than as an optimisation setting. The same distinction constrains compilers. An implementation may turn a non-strict argument into an eagerly evaluated one only where it can prove the expression terminates and has no observable effect. Those two proof obligations are exactly rows two and three of the table; without them the transformation would change what the program does. ## The limit of non-strictness Deferring does not repair the expression. It relocates the moment of truth: - If the body **does** demand the parameter, a failing expression fails — later, and from inside the callee rather than at the call site, which changes where a handler must sit and what context the failure carries. - A non-terminating expression still never terminates once demanded; it simply hangs at the demand point instead of at the call. - Caching, under call-by-need, does not help either: a result that never arrives is never stored. So the accurate claim is not "non-strictness makes bad arguments safe" but "non-strictness only pays for the arguments that are actually used". ## Why the shape keeps appearing Once you see it, the pattern is everywhere: use this value or that one if the first is missing; check this condition and here is the explanation to report if it fails; pick one of two branches and evaluate only the chosen one. Every one of these wants exactly one of its arguments evaluated, chosen by logic inside the callee. Under a strict language they cannot be written as ordinary functions with full fidelity — the unused branch is evaluated anyway — which is why such languages either build the shape into the language itself or offer a way to mark a parameter non-strict. ## What interviewers listen for The answer they want has two halves. First, the mechanism: strict evaluation happens before the body's logic, so the body's logic cannot protect anything. Second, the honest limit: deferral avoids the failure only while nothing demands the argument. A candidate who says non-strict evaluation "handles" or "catches" the error has the wrong model — there is nothing to catch, because nothing ran.
- Does non-strict evaluation remove the failure, or move it?It removes it only while nothing demands the argument. If the body does demand it, the same failure occurs, but at the demand point inside the callee rather than at the call site. That changes which handler sees it and what surrounding context is available, so the failure has been relocated rather than fixed.
- Why can a compiler not freely evaluate a non-strict argument early?Because the transformation is only safe when the expression provably terminates and has no observable effect. Without those two guarantees, evaluating early can turn a call that would have returned into one that fails or hangs, and can make an effect happen that the program never asked for. Strictness is part of the meaning, not a tuning knob.
saying these in an interview costs you the question
- Says a strict call skips an argument the body never reaches
- Believes non-strict evaluation catches the error rather than avoiding it
- Claims strictness only changes speed, never which calls complete
- Thinks a diverging expression terminates once it is demanded
- Assumes caching can rescue a result that never arrives