How can I terminate a function process when an event occurred

everyone, there is a task, where I need to terminate a function process when an event occurs.

Normally, when throwing an error in anything inside the process, for example, throwing an error from the delay in any step of the next steps, the process will terminate without any problems.

But if I created an event listener, that’s responsible for terminating the process. If I make throw error inside it the process will keep running without any problems in the process. and it will be considered as an Unhandled Rejection or DOMException.

  • Because the error thrown was made inside the listener context and not the main function context everything will keep going without any problems so to make it clear I will explain what I mean with some examples

  • In The first example I’m tring to terminate the process within the event listener, but the process will continue normally without problems and it is treated as an Unhandled Rejection or DOMException

(() => {
  class EventEmitter {
    constructor() {
      this.events = {};
    }

    on(event, listener) {
      if (!this.events[event]) {
        this.events[event] = [];
      }

      this.events[event].push(listener);

      // Return a function that can be used to remove this specific listener
      return () => {
        this.off(event, listener);
      };
    }

    emit(event, ...args) {
      if (this.events[event]) {
        this.events[event].forEach(listener => listener(...args));
      }
    }

    removeListeners(event) {
      delete this.events[event];
    }

    off(event, listener) {
      if (this.events[event]) {
        this.events[event] = this.events[event].filter(existingListener => existingListener !== listener);
      }
    }
  }

  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  const processEmitter = new EventEmitter();

  async function startProcess(params) {
    try {
      const removeListener = processEmitter.on('stop', async () => {
        removeListener(); 
        throw new Error(`Process stopped due to external event`); 
      });

      await delay(2000);
      console.log('finished step 1');
      await delay(2000);
      console.log('finished step 2');
      await delay(2000);
      console.log('finished step 3');
      await delay(2000);
      console.log('finished step 4');
      await delay(2000);
      console.log('finished step 5');
      await delay(2000);
      console.log('finished step 6');
    } catch (err) {
      console.log('Process terminated:', err.message);
    } finally {
      console.log('done !!')
    }
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();

  • In The following example I’m trying to throw the error with proxy when the value changed
(() =>  {
  const processEmitter = new EventEmitter();
  async function startProcess(params) {
    try {
      // use proxy as a middleware when the value change
      const abortHandler = new Proxy({ msg: '' }, {
        set(target, key, value) {
          target[key] = value;
          throw new Error(value);
        }
      });

      const removeListener = processEmitter.on('stop', async () => {
        removeListener(); 
        abortHandler.msg = `Process stopped due to external event`; 
      });

      await delay(2000);
      console.log('finished step 1');
      await delay(2000);
      console.log('finished step 2');
      await delay(2000);
      console.log('finished step 3');
      await delay(2000);
      console.log('finished step 4');
      await delay(2000);
      console.log('finished step 5');
      await delay(2000);
      console.log('finished step 6');
    } catch (err) {
      console.log('Process terminated:', err.message);
    } finally {
      console.log('done !!')
    }
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();
  • In the following example I’m trying to terminate the process with function that will throw the error when called but also it doesn’t work because it’s running inside the listener context not at the main function context
(() =>  {
  const processEmitter = new EventEmitter();
  async function startProcess(params) {
    try {
      // use proxy as a middleware when the value change
      const reject = msg => {
        throw new Error(msg);
      } 

      const removeListener = processEmitter.on('stop', async () => {
        removeListener(); 
        reject(`Process stopped due to external event`); 
      });

      await delay(2000);
      console.log('finished step 1');
      await delay(2000);
      console.log('finished step 2');
      await delay(2000);
      console.log('finished step 3');
      await delay(2000);
      console.log('finished step 4');
      await delay(2000);
      console.log('finished step 5');
      await delay(2000);
      console.log('finished step 6');
    } catch (err) {
      console.log('Process terminated:', err.message);
    } finally {
      console.log('done !!')
    }
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();
  • I also tried to wrap the whole function in a Promise and use reject with hope to stop the process when call reject but because of the Promise sync nature the process will keep going after calling reject without any problems
(() => {
  const processEmitter = new EventEmitter();
  async function startProcess(params) {
    return new Promise(async (_resolve, reject) => {
      try {
        const removeListener = processEmitter.on('stop', async () => {
          removeListener();
          reject(`Process stopped due to external event`);
        });

        await delay(2000);
        console.log('finished step 1');
        await delay(2000);
        console.log('finished step 2');
        await delay(2000);
        console.log('finished step 3');
        await delay(2000);
        console.log('finished step 4');
        await delay(2000);
        console.log('finished step 5');
        await delay(2000);
        console.log('finished step 6');
      } catch (err) {
        console.log('Process terminated:', err.message);
      } finally {
        console.log('done !!')
      }
    })
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();

  • I also tried intervals but the result is still the same because throwing the error will be within the callback context not at the main function context

  • The solution I found is a repetitive solution, which is the normal case, but I need to reduce repeating the same steps / code more than once, which is to use a variable or AbortController. However, these solutions will also be invalid, and it is not the best solution, because when I make a call to controller.abort() Also, step 3 is executed even though it is not supposed to be executed, because the stop event is called in the first second of the function call, meaning that step 3 is executed while it’s suppose to not, and after that it goes to step 4 and wow it findout that signal is aborted, so it throws an error + that this step has a lot of repeativitive code, meaning that If each delay represents a function alone with different code, then repeat the same step for all of them

(() =>  {
  const delay = (ms, signal) => {
    if (signal?.aborted) {
      throw new Error(signal?.reason || 'Aborted')
    }

    return new Promise(resolve => setTimeout(resolve, ms))
  };

  const processEmitter = new EventEmitter();
  const controller = new AbortController();
  const { signal } = controller;

  async function startProcess(params) {
    try {
      const removeListener = processEmitter.on('stop', async () => {
        removeListener();
        controller.abort('Process stopped due to external event');
      });

      await delay(2000, signal);
      console.log('finished step 1');
      await delay(2000, signal);
      console.log('finished step 2');
      await delay(2000, signal);
      console.log('finished step 3');
      await delay(2000, signal);
      console.log('finished step 4');
      await delay(2000, signal);
      console.log('finished step 5');
      await delay(2000, signal);
      console.log('finished step 6');
    } catch (err) {
      console.log('Process terminated:', err.message);
    } finally {
      console.log('done !!')
    }
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();
  • The last solution is variable, which is also the same idea as aborted signals, but the difference is that abort controller signals make the application logic better and more elegant, and you can use it in differnt ways.
(() =>  {
  const delay = (ms, err) => {
    if (err) {
      throw err;
    }

    return new Promise(resolve => setTimeout(resolve, ms))
  };

  const processEmitter = new EventEmitter();

  async function startProcess(params) {
    try {
      let err = null;
      const removeListener = processEmitter.on('stop', async () => {
        removeListener();
        err = new Error('Process stopped due to external event');
      });

      await delay(2000, err);
      console.log('finished step 1');
      await delay(2000, err);
      console.log('finished step 2');
      await delay(2000, err);
      console.log('finished step 3');
      await delay(2000, err);
      console.log('finished step 4');
      await delay(2000, err);
      console.log('finished step 5');
      await delay(2000, err);
      console.log('finished step 6');
    } catch (err) {
      console.log('Process terminated:', err.message);
    } finally {
      console.log('done !!')
    }
  }

  startProcess();

  setTimeout(() => {
    processEmitter.emit('stop');
  }, 5000);
})();