asynchronous programming
promises
continuation passing style
functor
monad

What are the advantages of Promises over CPS and the Continuation Functor/Monad?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Promises provide significant advantages over Continuation-Passing Style (CPS) and the Continuation Functor/Monad: they flatten callback nesting into chainable .then() calls, centralize error handling with .catch(), guarantee single resolution (a Promise settles exactly once), and offer built-in concurrency primitives (Promise.all, Promise.race). While CPS and continuation monads are more general and composable in functional programming, Promises are more practical for everyday async programming — especially in JavaScript where they integrate with async/await syntax.

Quick Comparison

FeatureCPSContinuation MonadPromises
ReadabilityDeep nestingRequires monad knowledgeFlat chains, async/await
Error handlingManual at every stepVia monad error handlingCentralized .catch()
CompositionManual chainingbind/>>=.then() chaining
Single resolutionNo guaranteeDepends on implementationGuaranteed (settle once)
ConcurrencyManualManualPromise.all, Promise.race
Language supportAny languageFunctional languagesNative in JS, adopted widely

CPS (Continuation-Passing Style)

In CPS, every async function takes a callback that receives the result:

javascript
1// CPS style — each step takes a callback
2function readFile(path, callback) {
3  fs.readFile(path, 'utf8', (err, data) => {
4    if (err) callback(err, null);
5    else callback(null, data);
6  });
7}
8
9function parseJSON(text, callback) {
10  try {
11    callback(null, JSON.parse(text));
12  } catch (err) {
13    callback(err, null);
14  }
15}
16
17// Using CPS — nested callbacks ("callback hell")
18readFile('config.json', (err, data) => {
19  if (err) { console.error(err); return; }
20  parseJSON(data, (err, config) => {
21    if (err) { console.error(err); return; }
22    connectDB(config.dbUrl, (err, db) => {
23      if (err) { console.error(err); return; }
24      db.query('SELECT * FROM users', (err, users) => {
25        if (err) { console.error(err); return; }
26        console.log(users);
27      });
28    });
29  });
30});

Every callback must manually check for errors. Adding a step means adding another nesting level.

The Same Thing with Promises

javascript
1// Promise style — flat chain
2readFile('config.json')
3  .then(data => parseJSON(data))
4  .then(config => connectDB(config.dbUrl))
5  .then(db => db.query('SELECT * FROM users'))
6  .then(users => console.log(users))
7  .catch(err => console.error(err));  // One error handler for all steps

The flat .then() chain replaces nesting with a linear pipeline. A single .catch() at the end handles errors from any step.

With async/await

javascript
1// async/await — reads like synchronous code
2async function loadUsers() {
3  try {
4    const data = await readFile('config.json');
5    const config = await parseJSON(data);
6    const db = await connectDB(config.dbUrl);
7    const users = await db.query('SELECT * FROM users');
8    console.log(users);
9  } catch (err) {
10    console.error(err);
11  }
12}

async/await is syntactic sugar over Promises. It provides the readability of synchronous code with the non-blocking behavior of async operations.

Advantage 1: Flat Composition

javascript
1// CPS — composition requires nesting
2function fetchAndProcess(url, callback) {
3  fetch(url, (err, response) => {
4    if (err) return callback(err);
5    parse(response, (err, data) => {
6      if (err) return callback(err);
7      transform(data, (err, result) => {
8        if (err) return callback(err);
9        callback(null, result);
10      });
11    });
12  });
13}
14
15// Promises — composition is linear
16function fetchAndProcess(url) {
17  return fetch(url)
18    .then(response => parse(response))
19    .then(data => transform(data));
20}

Each .then() returns a new Promise, enabling flat chaining regardless of how many steps you add.

Advantage 2: Centralized Error Handling

javascript
1// CPS — must handle errors at every step
2readFile(path, (err, data) => {
3  if (err) { handleError(err); return; }  // Error check 1
4  parse(data, (err, result) => {
5    if (err) { handleError(err); return; }  // Error check 2
6    save(result, (err) => {
7      if (err) { handleError(err); return; }  // Error check 3
8      console.log('Done');
9    });
10  });
11});
12
13// Promises — one catch handles all errors
14readFile(path)
15  .then(data => parse(data))
16  .then(result => save(result))
17  .then(() => console.log('Done'))
18  .catch(handleError);  // Catches errors from any step

Errors propagate down the Promise chain until a .catch() handles them — similar to synchronous try/catch.

Advantage 3: Single Resolution Guarantee

javascript
1// CPS — nothing prevents calling callback multiple times
2function riskyOperation(callback) {
3  callback(null, 'first');
4  callback(null, 'second');  // Oops — callback called twice
5}
6
7// Promises — settle exactly once
8function safeOperation() {
9  return new Promise((resolve, reject) => {
10    resolve('first');
11    resolve('second');  // Ignored — already resolved
12    reject('error');    // Also ignored
13  });
14}
15
16safeOperation().then(v => console.log(v));  // "first" — only once

A Promise transitions from pending to either fulfilled or rejected exactly once. Subsequent calls to resolve or reject are silently ignored.

Advantage 4: Built-In Concurrency

javascript
1// Run tasks in parallel
2const [users, posts, comments] = await Promise.all([
3  fetchUsers(),
4  fetchPosts(),
5  fetchComments()
6]);
7
8// Race — use whichever completes first
9const fastest = await Promise.race([
10  fetchFromServer1(),
11  fetchFromServer2()
12]);
13
14// Wait for all to settle (success or failure)
15const results = await Promise.allSettled([
16  fetchUsers(),
17  fetchPosts()
18]);
19// [{status: 'fulfilled', value: [...]}, {status: 'rejected', reason: Error}]
20
21// CPS equivalent of Promise.all — manual and error-prone
22let count = 0, results = [];
23function onComplete(index, data) {
24  results[index] = data;
25  if (++count === 3) processAll(results);
26}
27fetchUsers((e, d) => onComplete(0, d));
28fetchPosts((e, d) => onComplete(1, d));
29fetchComments((e, d) => onComplete(2, d));

Continuation Monad Comparison

The continuation monad is the theoretical foundation behind Promises:

haskell
1-- Haskell Continuation Monad
2newtype Cont r a = Cont { runCont :: (a -> r) -> r }
3
4instance Monad (Cont r) where
5  return a = Cont $ \k -> k a
6  (Cont m) >>= f = Cont $ \k -> m (\a -> runCont (f a) k)
7
8-- Usage
9example :: Cont r Int
10example = do
11  x <- return 5
12  y <- return 10
13  return (x + y)
14
15result = runCont example id  -- 15
javascript
1// JavaScript equivalent (conceptual)
2const cont = (value) => ({
3  bind: (fn) => fn(value),
4  map: (fn) => cont(fn(value))
5});
6
7// Promise is essentially a cont monad with:
8// - Async resolution
9// - Error channel (reject)
10// - Single-resolution guarantee
11// - Built-in concurrency combinators

Promises are a specialized, practical version of the continuation monad. The continuation monad is more general (works for any control flow), but Promises are more ergonomic for async operations.

Common Pitfalls

  • CPS error handling discipline: In CPS, forgetting to check err in any callback silently drops errors. Promises propagate errors automatically — you must actively ignore them (no .catch()), which linters can detect.
  • Promise constructor anti-pattern: Wrapping an existing Promise in new Promise() is unnecessary: new Promise(resolve => resolve(fetch(url))) should be just fetch(url). Only use new Promise to wrap callback-based APIs.
  • Unhandled rejections: Unlike CPS where errors are silently lost, unhandled Promise rejections trigger warnings (Node.js) or errors (browsers). Always add .catch() or use try/catch with async/await.
  • Continuation monad learning curve: The continuation monad requires understanding monadic composition (bind, >>=), which has a steep learning curve compared to .then() chains. Promises trade some generality for accessibility.
  • Mixing CPS and Promises: Converting between CPS and Promises (with util.promisify or manual wrapping) is common but error-prone. Ensure the callback follows the (error, result) convention, or the conversion will not work correctly.

Summary

  • Promises flatten callback nesting into linear .then() chains
  • A single .catch() handles errors from any step in the chain — no manual checking at each callback
  • Promises guarantee single resolution — a Promise settles exactly once, preventing double-callback bugs
  • Built-in Promise.all, Promise.race, and Promise.allSettled handle concurrency without manual coordination
  • async/await syntax makes Promise code read like synchronous code
  • The continuation monad is more general but less practical — Promises are a specialized, ergonomic version for async programming

Course illustration
Course illustration

All Rights Reserved.