current commit
260903.7 · domain: runtime-deprecate the module
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

// WARNING: THIS MODULE IS DEPRECATED (DEP0032).
//
// Loading this module emits a runtime deprecation warning.
// No new pull requests targeting this module will be accepted
// unless they address existing, critical bugs.

const {
  ArrayPrototypeIndexOf,
  ArrayPrototypeLastIndexOf,
  ArrayPrototypePush,
  ArrayPrototypeSlice,
  ArrayPrototypeSplice,
  Error,
  FunctionPrototypeCall,
  ObjectDefineProperty,
  ReflectApply,
  SafeSet,
} = primordials;

const EventEmitter = require('events');
const {
  ERR_UNHANDLED_ERROR,
} = require('internal/errors').codes;

// The `domain` module is deprecated (DEP0032). Emit a runtime deprecation
// warning when the module is loaded.
process.emitWarning('The `domain` module is deprecated and should not be used.',
                    'DeprecationWarning', 'DEP0032');

// Use AsyncLocalStorage for domain context propagation
// Lazy initialization - ALS is created on first domain use
let domainStorage;

function initDomainStorage() {
  if (domainStorage === undefined) {
    const { AsyncLocalStorage } = require('async_hooks');

    // Single AsyncLocalStorage instance for all domain context
    // Store structure: { domain: Domain | null, stack: Domain[] }
    domainStorage = new AsyncLocalStorage();
  }
  return domainStorage;
}

// Helper functions to get/set domain store
function getDomainStore() {
  return initDomainStorage().getStore();
}

function setDomainStore(store) {
  initDomainStorage().enterWith(store);
}

// Overwrite process.domain with a getter/setter backed by native context
ObjectDefineProperty(process, 'domain', {
  __proto__: null,
  enumerable: true,
  get: function() {
    const store = getDomainStore();
    // Return undefined when no domain is active (store doesn't exist OR domain is null)
    // This makes the API consistent: undefined = no domain, Domain object = domain active
    const domain = store?.domain;
    return domain === null ? undefined : domain;
  },
  set: function(arg) {
    const store = getDomainStore();
    if (store) {
      // Create new store to avoid affecting other async contexts
      setDomainStore({ domain: arg, stack: store.stack });
    }
    // If no store exists, setting is a no-op (will be set when domain is entered)
    return arg;
  },
});

// Domain uses the stacking capability of setUncaughtExceptionCaptureCallback
// to coexist with other callbacks (e.g., REPL).
const { setUncaughtExceptionCaptureCallback } = process;


// Helper to get the current stack from native context
function getStack() {
  const store = getDomainStore();
  return store?.stack ?? [];
}

// Helper to set the stack in native context
function setStack(newStack) {
  const store = getDomainStore();
  if (store) {
    // Create new store to avoid affecting other async contexts
    setDomainStore({ domain: store.domain, stack: newStack });
  }
}

// It's possible to enter one domain while already inside
// another one. The stack is each entered domain.
// For backward compatibility, exports._stack is a getter/setter
ObjectDefineProperty(exports, '_stack', {
  __proto__: null,
  enumerable: true,
  get: getStack,
  set: setStack,
});

// Module-level tracking of the current domain and stack for exception handling.
// This is needed because when exceptions propagate out of domainStorage.run(),
// the ALS context is restored before the exception capture callback runs.
// For async callbacks, ALS context is still available, but for synchronous
// exceptions we need this fallback.
let currentDomain = null;
let currentStack = null;  // The stack at the time of the current sync operation

// Track all domains that have error listeners. This is needed because async
// callbacks may fire with a domain context even when the domain is not on the
// current stack. We install the exception handler when ANY domain has listeners.
const domainsWithListeners = new SafeSet();

// The domain exception handler - only installed when a domain with error listeners
// exists. This allows uncaughtException event to fire when no handler.
function domainExceptionHandler(er) {
  // For sync exceptions (from run/bound/intercepted), currentDomain is set to the
  // domain that was active when the exception was thrown. Check it first because
  // by the time we get here, the ALS context may have already been restored to the
  // outer domain.
  // For async callbacks, currentDomain is null and process.domain reads from ALS.
  const domain = currentDomain || process.domain;

  // For sync exceptions, we need to restore the ALS context before calling _errorHandler
  // because ALS unwinds during exception propagation. This ensures exit() works correctly.
  if (currentDomain !== null && currentStack !== null) {
    // Restore the ALS context and exports.active to match the state at the time of the throw
    setDomainStore({ domain: currentDomain, stack: currentStack });
    exports.active = currentDomain;
  }

  // First check the current domain
  if (domain && domain.listenerCount('error') > 0) {
    return domain._errorHandler(er);
  }

  // For SYNC exceptions only (currentDomain is set), check parent domains on the stack.
  // This handles the case where d2 (no handler) throws synchronously inside d (has handler).
  // For ASYNC exceptions (currentDomain is null), only the domain from ALS matters -
  // the exception should NOT bubble up to parent domains that were on the stack when
  // the async callback was scheduled.
  if (currentDomain !== null && currentStack !== null) {
    for (let i = currentStack.length - 1; i >= 0; i--) {
      const parentDomain = currentStack[i];
      if (parentDomain !== domain && parentDomain.listenerCount('error') > 0) {
        return parentDomain._errorHandler(er);
      }
    }
  }

  // No domain with error handler found - let the exception propagate.
  // Since setUncaughtExceptionCaptureCallback ignores our return value,
  // we must emit 'uncaughtException' manually to give other handlers a chance.
  // We temporarily uninstall ourselves to avoid infinite recursion if someone
  // throws from their uncaughtException handler.
  uninstallExceptionHandler();

  // Emit uncaughtException to give process.on('uncaughtException') handlers a chance
  const handled = process.emit('uncaughtException', er, 'uncaughtException');

  if (handled) {
    // Reinstall ourselves for future exceptions
    installExceptionHandler();
  } else {
    // No handler caught the exception - we need to exit.
    // Throw it to let the C++ side handle the exit properly (respects --abort-on-uncaught).
    // Don't reinstall the callback - we want the throw to propagate directly to C++.
    throw er;
  }
}

// Track if exception handler is currently installed
let exceptionHandlerInstalled = false;

function installExceptionHandler() {
  if (!exceptionHandlerInstalled) {
    setUncaughtExceptionCaptureCallback(domainExceptionHandler);
    exceptionHandlerInstalled = true;
  }
}

function uninstallExceptionHandler() {
  if (exceptionHandlerInstalled) {
    setUncaughtExceptionCaptureCallback(null);
    exceptionHandlerInstalled = false;
  }
}

function updateExceptionCapture() {
  // Install exception handler if ANY domain has error listeners
  if (domainsWithListeners.size > 0) {
    installExceptionHandler();
  } else {
    uninstallExceptionHandler();
  }
}

// Called when an 'error' listener is being added
// 'newListener' is called BEFORE listener is added, so count is still 0
function domainNewListener(type) {
  if (type === 'error') {
    domainsWithListeners.add(this);
    updateExceptionCapture();
  }
}

// Called when an 'error' listener is being removed
// 'removeListener' is called AFTER listener is removed, so count reflects new state
function domainRemoveListener(type) {
  if (type === 'error' && this.listenerCount('error') === 0) {
    domainsWithListeners.delete(this);
    updateExceptionCapture();
  }
}


process.on('newListener', (name, listener) => {
  if (name === 'uncaughtException' &&
      listener !== domainUncaughtExceptionClear) {
    // Make sure the first listener for `uncaughtException` always clears
    // the domain stack.
    process.removeListener(name, domainUncaughtExceptionClear);
    process.prependListener(name, domainUncaughtExceptionClear);
  }
});

process.on('removeListener', (name, listener) => {
  if (name === 'uncaughtException' &&
      listener !== domainUncaughtExceptionClear) {
    // If the domain listener would be the only remaining one, remove it.
    const listeners = process.listeners('uncaughtException');
    if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear)
      process.removeListener(name, domainUncaughtExceptionClear);
  }
});

function domainUncaughtExceptionClear() {
  // Clear the stack and active domain in the CURRENT context only
  // Don't mutate the store in-place as it would affect other async contexts
  // that share the same store reference
  setDomainStore({ domain: null, stack: [] });
  exports.active = null;
  currentDomain = null;
  currentStack = null;
  updateExceptionCapture();
}


class Domain extends EventEmitter {
  constructor() {
    super();

    this.members = [];

    this.on('newListener', domainNewListener);
    this.on('removeListener', domainRemoveListener);
  }
}

exports.Domain = Domain;

exports.create = exports.createDomain = function createDomain() {
  return new Domain();
};

// The active domain is always the one that we're currently in.
exports.active = null;
Domain.prototype.members = undefined;

// Called by process._fatalException in case an error was thrown.
Domain.prototype._errorHandler = function(er) {
  let caught = false;

  if ((typeof er === 'object' && er !== null) || typeof er === 'function') {
    ObjectDefineProperty(er, 'domain', {
      __proto__: null,
      configurable: true,
      enumerable: false,
      value: this,
      writable: true,
    });
    er.domainThrown = true;
  }
  // Pop all adjacent duplicates of the currently active domain from the stack.
  // This is done to prevent a domain's error handler to run within the context
  // of itself, and re-entering itself recursively handler as a result of an
  // exception thrown in its context.
  while (exports.active === this) {
    this.exit();
  }

  const stack = getStack();

  // The top-level domain-handler is handled separately.
  //
  // The reason is that if V8 was passed a command line option
  // asking it to abort on an uncaught exception (currently
  // "--abort-on-uncaught-exception"), we want an uncaught exception
  // in the top-level domain error handler to make the
  // process abort. Using try/catch here would always make V8 think
  // that these exceptions are caught, and thus would prevent it from
  // aborting in these cases.
  if (stack.length === 0) {
    // If there's no error handler, do not emit an 'error' event
    // as this would throw an error, make the process exit, and thus
    // prevent the process 'uncaughtException' event from being emitted
    // if a listener is set.
    if (this.listenerCount('error') > 0) {
      // Clear the uncaughtExceptionCaptureCallback so that we know that, since
      // the top-level domain is not active anymore, it would be ok to abort on
      // an uncaught exception at this point
      uninstallExceptionHandler();
      try {
        caught = this.emit('error', er);
      } finally {
        installExceptionHandler();
      }
    }
  } else {
    // Wrap this in a try/catch so we don't get infinite throwing
    try {
      // One of three things will happen here.
      //
      // 1. There is a handler, caught = true
      // 2. There is no handler, caught = false
      // 3. It throws, caught = false
      //
      // If caught is false after this, then there's no need to exit()
      // the domain, because we're going to crash the process anyway.
      caught = this.emit('error', er);
    } catch (er2) {
      // The domain error handler threw!  oh no!
      // See if another domain can catch THIS error,
      // or else crash on the original one.
      updateExceptionCapture();
      const currentStack = getStack();
      if (currentStack.length) {
        const parentDomain = currentStack[currentStack.length - 1];
        const store = getDomainStore();
        if (store) {
          // Create new store to avoid affecting other async contexts
          setDomainStore({ domain: parentDomain, stack: store.stack });
        }
        exports.active = parentDomain;
        caught = parentDomain._errorHandler(er2);
      } else {
        // Pass on to the next exception handler.
        throw er2;
      }
    }
  }

  // Exit all domains on the stack.  Uncaught exceptions end the
  // current tick and no domains should be left on the stack
  // between ticks.
  domainUncaughtExceptionClear();

  return caught;
};


Domain.prototype.enter = function() {
  // Note that this might be a no-op, but we still need
  // to push it onto the stack so that we can pop it later.
  const currentStore = getDomainStore() || { domain: null, stack: [] };
  const newStack = ArrayPrototypeSlice(currentStore.stack);
  ArrayPrototypePush(newStack, this);
  setDomainStore({ domain: this, stack: newStack });
  exports.active = this;
  currentDomain = this;
  currentStack = newStack;
  updateExceptionCapture();
};


Domain.prototype.exit = function() {
  // Don't do anything if this domain is not on the stack.
  const store = getDomainStore();
  if (!store) return;

  const stack = store.stack;
  const index = ArrayPrototypeLastIndexOf(stack, this);
  if (index === -1) return;

  // Exit all domains until this one.
  const newStack = ArrayPrototypeSlice(stack, 0, index);
  const parentDomain = newStack.length === 0 ? null : newStack[newStack.length - 1];
  setDomainStore({ domain: parentDomain, stack: newStack });
  exports.active = parentDomain;
  currentDomain = parentDomain;
  currentStack = newStack.length === 0 ? null : newStack;
  updateExceptionCapture();
};


// note: this works for timers as well.
Domain.prototype.add = function(ee) {
  // If the domain is already added, then nothing left to do.
  if (ee.domain === this)
    return;

  // Has a domain already - remove it first.
  if (ee.domain)
    ee.domain.remove(ee);

  // Check for circular Domain->Domain links.
  // They cause big issues.
  //
  // For example:
  // var d = domain.create();
  // var e = domain.create();
  // d.add(e);
  // e.add(d);
  // e.emit('error', er); // RangeError, stack overflow!
  if (this.domain && (ee instanceof Domain)) {
    for (let d = this.domain; d; d = d.domain) {
      if (ee === d) return;
    }
  }

  ObjectDefineProperty(ee, 'domain', {
    __proto__: null,
    configurable: true,
    enumerable: false,
    value: this,
    writable: true,
  });
  ArrayPrototypePush(this.members, ee);
};


Domain.prototype.remove = function(ee) {
  ee.domain = null;
  const index = ArrayPrototypeIndexOf(this.members, ee);
  if (index !== -1)
    ArrayPrototypeSplice(this.members, index, 1);
};


// Helper to run a function within a domain context.
// Uses enterWith instead of ALS.run() so the context is NOT automatically
// restored on exception - this matches original domain behavior where
// exit() only runs on success.
function runInDomain(domain, thisArg, fn, args) {
  const currentStore = getDomainStore() || { domain: null, stack: [] };
  const newStack = ArrayPrototypeSlice(currentStore.stack);
  ArrayPrototypePush(newStack, domain);
  const previousDomain = currentDomain;
  const previousStack = currentStack;

  currentDomain = domain;
  currentStack = newStack;
  updateExceptionCapture();

  setDomainStore({ domain, stack: newStack });
  exports.active = domain;

  const result = ReflectApply(fn, thisArg, args);

  // On success, restore context (if exception thrown, context stays for catch blocks)
  setDomainStore(currentStore);
  exports.active = currentStore.domain;
  currentDomain = previousDomain;
  currentStack = previousStack;
  updateExceptionCapture();

  return result;
}

Domain.prototype.run = function(fn) {
  return runInDomain(this, this, fn, ArrayPrototypeSlice(arguments, 1));
};


function intercepted(_this, self, cb, fnargs) {
  if (fnargs[0] && fnargs[0] instanceof Error) {
    const er = fnargs[0];
    er.domainBound = cb;
    er.domainThrown = false;
    ObjectDefineProperty(er, 'domain', {
      __proto__: null,
      configurable: true,
      enumerable: false,
      value: self,
      writable: true,
    });
    self.emit('error', er);
    return;
  }

  return runInDomain(self, _this, cb, ArrayPrototypeSlice(fnargs, 1));
}


Domain.prototype.intercept = function(cb) {
  const self = this;

  function runIntercepted() {
    return intercepted(this, self, cb, arguments);
  }

  return runIntercepted;
};


function bound(_this, self, cb, fnargs) {
  return runInDomain(self, _this, cb, fnargs);
}


Domain.prototype.bind = function(cb) {
  const self = this;

  function runBound() {
    return bound(this, self, cb, arguments);
  }

  ObjectDefineProperty(runBound, 'domain', {
    __proto__: null,
    configurable: true,
    enumerable: false,
    value: this,
    writable: true,
  });

  return runBound;
};

// Override EventEmitter methods to make it domain-aware.
EventEmitter.usingDomains = true;

const eventInit = EventEmitter.init;
EventEmitter.init = function(opts) {
  ObjectDefineProperty(this, 'domain', {
    __proto__: null,
    configurable: true,
    enumerable: false,
    value: null,
    writable: true,
  });
  if (exports.active && !(this instanceof exports.Domain)) {
    this.domain = exports.active;
  }

  return FunctionPrototypeCall(eventInit, this, opts);
};

const eventEmit = EventEmitter.prototype.emit;
EventEmitter.prototype.emit = function emit(...args) {
  const domain = this.domain;

  const type = args[0];
  const shouldEmitError = type === 'error' &&
                          this.listenerCount(type) > 0;

  // Just call original `emit` if current EE instance has `error`
  // handler, there's no active domain or this is process
  if (shouldEmitError || domain === null || domain === undefined ||
      this === process) {
    return ReflectApply(eventEmit, this, args);
  }

  if (type === 'error') {
    const er = args.length > 1 && args[1] ?
      args[1] : new ERR_UNHANDLED_ERROR();

    if (typeof er === 'object') {
      er.domainEmitter = this;
      ObjectDefineProperty(er, 'domain', {
        __proto__: null,
        configurable: true,
        enumerable: false,
        value: domain,
        writable: true,
      });
      er.domainThrown = false;
    }

    // Remove the current domain (and its duplicates) from the domains stack and
    // set the active domain to its parent (if any) so that the domain's error
    // handler doesn't run in its own context. This prevents any event emitter
    // created or any exception thrown in that error handler from recursively
    // executing that error handler.
    const store = getDomainStore();
    const stack = store?.stack ?? [];
    const origDomainsStack = ArrayPrototypeSlice(stack);
    const origActiveDomain = store?.domain ?? null;

    // Travel the domains stack from top to bottom to find the first domain
    // instance that is not a duplicate of the current active domain.
    let idx = stack.length - 1;
    while (idx > -1 && origActiveDomain === stack[idx]) {
      --idx;
    }

    // Change the stack to not contain the current active domain, and only the
    // domains above it on the stack.
    let newStack;
    if (idx < 0) {
      newStack = [];
    } else {
      newStack = ArrayPrototypeSlice(stack, 0, idx + 1);
    }

    // Change the current active domain
    const newActiveDomain = newStack.length > 0 ? newStack[newStack.length - 1] : null;
    const prevCurrentDomain = currentDomain;
    const prevCurrentStack = currentStack;

    if (store) {
      setDomainStore({ domain: newActiveDomain, stack: newStack });
    }
    exports.active = newActiveDomain;
    currentDomain = newActiveDomain;
    currentStack = newStack;

    updateExceptionCapture();

    domain.emit('error', er);

    // Now that the domain's error handler has completed, restore the domains
    // stack and the active domain to their original values.
    if (store) {
      setDomainStore({ domain: origActiveDomain, stack: origDomainsStack });
    }
    exports.active = origActiveDomain;
    currentDomain = prevCurrentDomain;
    currentStack = prevCurrentStack;
    updateExceptionCapture();

    return false;
  }

  domain.enter();
  const ret = ReflectApply(eventEmit, this, args);
  domain.exit();

  return ret;
};