Continuations

June 13, 2026

Formal definition

Let's define that a continuation after its execution returns a result, that can either be:

  • — a signal of computation end with a value
  • — a signal to pause the computation at this moment and then, some time after, continue with , where is a continuation
  • — a signal to pause the computation, perform a side effect and then resume with continuation , passing there the result of .

The notation defines a handler collection that maps effects to code that actually implements them. defines an application of such handler so that is computed and effects are handled as defined.

will be called the domain of handler , ia et of all effect types it handles. I.e. for , .

is a function that handles the effect . It always has a form of , i.e. a two-argument function that takes the effect to be performed and the continuation to be run afterwards.

The handler application semantics are as follows (assuming was reduced):

When a handler is applied to a end of computation, it immediately returns the same end of computation.

When a handler is applied to a paused computation, it reapplies itself to the result of this paused computation, to handle next effects.

When a handler is applied to a effect request, which effect is in its domain, it takes the function that handles the effect, applies it to the effect and continuation. The result of this application is the result of the handler. Then it reapplies itself to the result of this paused computation, to handle next effects.

When a handler is applied to a effect request, which effect is not in its domain, the handler immediately returns with a request for same effect, but schedules itself after the effect is performed. This (instead of just returning the same request) ensures that if situations with partial nested handlers do not produce bugs: if there is and first yields an effect request and then yields , then may not be handled without this rule.

Then we define a top-level handler (also called the "main" handler):

As there is no handler that can be on the upper level, then any effect should produce a run-time error there.

Typescript implementation

type Result =
  | { type: "finish"; value: any }
  | { type: "paused"; continue: () => Result }
  | { type: "effect"; effect: Effect; continue: (result: any) => Result };

type EffectType = "input" | "output" | "error";
type Effect = { type: EffectType; args: any };

function handle(
  handlers: Record<
    EffectType,
    (eff: Effect, then: (result: any) => Result) => Result
  >,
  eff: Result,
): any {
  while (true) {
    if (eff.type === "finish") return eff;
    else if (eff.type === "effect") {
      const handler = handlers[eff.effect.type];
      if (handler) eff = handler(eff.effect, eff.continue);
      // need to wrap to prevent escaping of effects
      else
        return perform(eff.effect.type, eff.effect.args, (res) =>
          handle(handlers, eff.continue(res)),
        );
    } else if (eff.type === "paused") eff = eff.continue();
  }
}

function runMain(eff: Result): any {
  const res = handle({}, eff);
  if (res.type === "finish") return res.value;
  if (res.type === "effect") {
    if (res.effect.type === "error") throw new Error(res.effect.args);
    else throw new Error("unhandled effect " + res.effect.type);
  }
  return null;
}

function perform(
  type: EffectType,
  args: any,
  then: (result: any) => Result,
): Result {
  return { type: "effect", effect: { type: type, args: args }, continue: then };
}

function endWith(value: any): Result {
  return { type: "finish", value: value };
}

declare global {
  interface Object {
    run(eff: Result): any;
  }
}

Object.prototype.run = function (this, eff) {
  return handle(this, eff);
};

const f1 = () =>
  perform("input", {}, (inputData) =>
    perform("error", inputData, () =>
      perform("output", inputData, () => endWith(inputData)),
    ),
  );

const ignoreErrors = (res: Result) =>
  ({
    error: (eff, then) =>
      perform("output", "ignored error " + eff.args + ", proceeding", then),
  }).run(res);

const catchErrors = (res: Result, onError: (err: any) => Result) =>
  ({
    error: (eff, then) => onError(eff.args),
  }).run(res);

const main = () =>
  catchErrors(f1(), (err) => perform("output", "There was an error!" + err, (_) => endWith(null)));

runMain(
  {
    input: (eff, then) => then("data"),
    output: (eff, then) => {
      console.log(eff.args);
      return then(null);
    },
  }.run(main()),
);