denoland/deno · error · ERR_INVALID_REPL_EVAL_CONFIG

ERR_INVALID_REPL_EVAL_CONFIG

ERR_INVALID_REPL_EVAL_CONFIG

Error message

Cannot specify both "breakEvalOnSigint" and "eval" for REPL

What it means

`repl.start()` accepts `breakEvalOnSigint` (abort the current evaluation on Ctrl+C) and `eval` (a custom function used to evaluate each line). They are mutually exclusive: the sigint-breaking mechanism only wraps the default eval path, so both Node and this polyfill throw ERR_INVALID_REPL_EVAL_CONFIG from the REPLServer constructor when both options are truthy.

Source

Thrown at ext/node/polyfills/repl.ts:705

    this._isStandalone =
      !!(options as Record<symbol, unknown>)[kStandaloneREPL];
    this.useGlobal = !!useGlobal;
    this.ignoreUndefined = !!ignoreUndefined;
    this.replMode = replMode || REPL_MODE_SLOPPY;
    this.underscoreAssigned = false;
    this.last = undefined;
    this.underscoreErrAssigned = false;
    this.lastError = undefined;
    this.breakEvalOnSigint = !!options.breakEvalOnSigint;
    this.editorMode = false;
    this[kContextId] = undefined;
    this._initialPrompt = prompt as string;
    // Stable resource name like Node's "REPL1"/"REPL2"/... - used as the
    // filename for compiled scripts so stack traces match Node's output.
    this._resourceName = getREPLResourceName();

    if (this.breakEvalOnSigint && eval_) {
      throw new ERR_INVALID_REPL_EVAL_CONFIG();
    }

    if ((options as Record<symbol, unknown>)[kStandaloneREPL]) {
      _module.exports.repl = this;
    } else {
      // Non-standalone REPLs install the user-listener guard and route
      // async errors via `process._fatalException`. The fatal-exception
      // hook is global; the guard is refcounted so multiple concurrent
      // REPLs co-exist and the process is left clean once the last exits.
      // We use `_fatalException` (rather than process.on) so user code
      // inspecting `process.listenerCount('uncaughtException')` sees 0,
      // matching Node's domain-based REPL.
      _installCaptureHook();
      _addNewListenerGuard();
      ArrayPrototypePush(_activeREPLs, this);
      this._processListenersAttached = true;
      this.once("exit", () => {
        const idx = ArrayPrototypeIndexOf(_activeREPLs, this);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Remove `breakEvalOnSigint: true` if you supply a custom `eval` function
  2. Drop the custom `eval` and use the default evaluator to keep Ctrl+C breaking
  3. Implement interruption yourself: set a flag from a SIGINT listener and check it inside the custom eval function

Example fix

// before
repl.start({ breakEvalOnSigint: true, eval: myEval });
// ERR_INVALID_REPL_EVAL_CONFIG

// after
repl.start({ eval: myEval }); // handle Ctrl+C inside myEval if needed
Defensive patterns

Strategy: validation

Validate before calling

function startReplSafely(opts = {}) {
  if (opts.breakEvalOnSigint && typeof opts.eval === 'function') {
    throw new TypeError(
      'breakEvalOnSigint and eval are mutually exclusive for repl.start()',
    );
  }
  return repl.start(opts);
}

Prevention

When it happens

Trigger: Calling `repl.start({ breakEvalOnSigint: true, eval: myEvalFn })`, or constructing `new REPLServer(...)` with both options set.

Common situations: Custom REPLs built on a vm/global-eval function where Ctrl+C interruption is also wanted; merging options objects copied from two different examples; adding `breakEvalOnSigint` to existing config without removing a previously-set `eval`.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/dc72842e6f097b51. Report an issue: GitHub.