Introduction

When you call a function, you expect it to return — to hand control back to whoever called it. That feels natural because it mirrors how human conversation works: you ask, they answer, you continue. But this implicit "return to caller" is actually a hidden assumption baked into the language runtime. Continuation-passing style (CPS) makes that assumption visible by turning it into an ordinary function argument.

In CPS, no function ever returns. Instead, every function receives an extra argument called the continuation — a function that represents what to do next. When the function finishes its work, it calls the continuation with the result rather than returning. The continuation is the entire future of the computation, packaged into a callable value.

The idea was formalized in the 1970s, first appearing in Scheme and later explored deeply by Gerald Jay Sussman and Guy L. Steele Jr. in their work on the Lambda Papers (1975–1980). Their insight was that continuations are first-class: they can be stored, passed around, and invoked — which means every control-flow mechanism (returns, exceptions, early exits, coroutines, backtracking) can be expressed as a function call.

This is not a curiosity for language theorists. Modern async/await, generators, and even the halting problem all trace back to the same question: what exactly is "what happens next"?

Try It

The demo below shows both styles side by side. A small pipeline computes a value through three steps: double, add ten, and square. In direct style each step returns to the caller. In CPS each step receives a continuation and calls it instead of returning.

<!-- {{c_main_comment}} -->
<div class="panel" id="direct-panel">
  <div class="panel-title">{{label_direct}}</div>
  <div class="pipeline" id="direct-pipeline"></div>
  <div class="result" id="direct-result"></div>
</div>
<div class="panel" id="cps-panel">
  <div class="panel-title">{{label_cps}}</div>
  <div class="pipeline" id="cps-pipeline"></div>
  <div class="result" id="cps-result"></div>
</div>
<div class="controls">
  <label for="input-val">{{label_input}}</label>
  <input id="input-val" type="number" value="3" min="1" max="9" title="{{input_title}}">
  <button id="btn-run" type="button">{{btn_run}}</button>
  <button id="btn-trace" type="button">{{btn_trace}}</button>
  <button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div class="log" id="log" aria-label="{{log_aria}}"></div>
/* {{c_css_comment}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panel { background: #f3f6f9; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .7rem; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase;
               letter-spacing: .08em; color: #5a7088; margin-bottom: .5rem; }
.pipeline { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; }
.step { display: flex; align-items: center; gap: .3rem; }
.box { padding: .3rem .65rem; border-radius: 7px; font: 600 .85rem ui-monospace, monospace;
       background: #dde4ec; border: 1.5px solid #b8c5d0; color: #1d3557;
       transition: background .25s, border-color .25s; }
.box.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.box.done   { background: #0a7d33; border-color: #0a7d33; color: #fff; }
.arrow { color: #8fa4b8; font-size: 1.1rem; user-select: none; }
.result { font: 600 .95rem ui-monospace, monospace; color: #0a7d33; margin-top: .5rem;
          min-height: 1.3em; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
label { font-size: .9rem; font-weight: 600; }
input[type=number] { width: 3.5rem; padding: .35rem .5rem; border: 1.5px solid #b8c5d0;
                     border-radius: 7px; font: 600 1rem ui-monospace, monospace;
                     text-align: center; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
         border: 1.5px solid #1d3557; background: #1d3557; color: #fff;
         border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { background: #1a1e26; color: #a8d8a8; border-radius: 8px; padding: .6rem .9rem;
       font: .82rem ui-monospace, monospace; min-height: 3rem; white-space: pre-wrap;
       max-height: 8rem; overflow-y: auto; }
// Code not found

Notice that in CPS the nesting depth grows with each step — the continuation of step 2 wraps the continuation of step 3. This is the famous callback pyramid. The computation is identical; only the shape of the code changes. When you press Trace you can watch the chain of continuation calls unroll left-to-right instead of returning right-to-left.

The Real Power

CPS looks like an odd coding style until you realize it is a complete program transformation — every program can be mechanically rewritten into CPS without changing what it computes.

  • Tail calls become jumps. In CPS every call is a tail call (the function calls its continuation as its last act and never returns). A compiler that recognizes this can replace the call with a simple jump, eliminating stack growth entirely. This is why Scheme mandates tail-call optimization: CPS makes it trivially detectable.
  • Exceptions are just continuations. Pass two continuations — one for success, one for failure — and throw becomes "call the failure continuation." No special exception machinery is needed in the language runtime.
  • Async/await is CPS in disguise. await suspends the current function and installs a continuation that resumes when the result is ready. The async runtime is a scheduler for continuations.
  • call/cc captures the entire future. Scheme's call-with-current-continuation (call/cc) lets you grab the continuation at any point and call it later — or multiple times. This makes it possible to implement coroutines, backtracking search, and even goto as pure library functions.

The price is verbosity: direct code that reads top-to-bottom becomes deeply nested or requires explicit threading of continuations. That is why languages like JavaScript introduced async/await — to hide CPS behind syntactic sugar while keeping the underlying model. The related technique of effect systems provides a type-safe way to track which continuations a function may call.

Where It Matters

Once you see CPS you find it everywhere:

  • Compiler intermediate representations: many production compilers (GHC for Haskell, SML/NJ, Chicken Scheme) convert their AST to a CPS IR before optimization. In CPS form every optimization — inlining, dead-code elimination, closure conversion — becomes a local rewrite rule.
  • JavaScript's event loop: every setTimeout, fetch, and addEventListener callback is a continuation. Node.js's original callback-first API was explicit CPS; Promises and async/await are syntactic layers on top.
  • Backtracking and logic programming: in Prolog-style search, when a branch fails you "restore" a saved continuation. Storing continuations in a stack is exactly what choice points are.
  • Delimited continuations: modern research gives you continuations that only capture part of the future, enabling algebraic effects — a clean way to model I/O, state, and exceptions without monads.
  • Type theory: the double-negation translation shows that classical logic corresponds to CPS-transformed intuitionistic logic. "A is true" becomes "given a proof of A, derive a contradiction" — a continuation.

The style also shows up in gradual typing: type checkers that must handle unknown types often thread a "what to do if this type is wrong" continuation through their inference algorithm.

Conclusion

Continuation-passing style is one of those ideas that looks baroque at first and then becomes impossible to unsee. By refusing to let functions return — by forcing them to call what comes next — CPS turns control flow from an invisible mechanism of the runtime into an ordinary value you can store, pass, and reason about.

Every await you write, every Promise chain, every Scheme tail call, every backtracking logic program is built on the same insight: the future of a computation is just a function. Grab it, schedule it, duplicate it, discard it — control is yours.

The next time you nest a callback inside a callback inside a callback and feel the pyramid pressing in on you, remember: you are not writing bad code. You are writing CPS by hand, and the halting problem itself is ultimately a statement about what continuations can and cannot express.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/continuation-passing-style/Content licensed under CC BY-NC 4.0.