Since the OP explicitly did ask for "simulating ... break ... inside ... forEach", and since the language core now has much more features than 11½ years ago, one actually could implement quite easily a prototypal array method which not only enables a break but also a continue command, similar to both statements break and continue.
In order to achieve the implementation of the iterating array method, one first needs to write some abstractions which do borrow the basic idea from AbortController and its related AbortSignal.
Thus, one would implement e.g. a PausedStateSignal ...
class PausedStateSignal extends EventTarget {
// shared protected/private state.
#state;
constructor(connect) {
super();
this.#state = {
isPaused: false,
};
connect(this, this.#state);
}
get paused() {
return this.#state.isPaused;
}
}
... which is going to be used by its PauseController ...
class PauseController {
#signal;
#signalState;
constructor() {
new PausedStateSignal((signal, signalState) => {
this.#signal = signal;
this.#signalState = signalState;
});
this.#signalState.isPaused = false;
}
get signal() {
return this.#signal;
}
break() {
const isPaused = this.#signalState.isPaused;
if (!isPaused) {
this.#signalState.isPaused = true;
}
this.#signal.dispatchEvent(
new CustomEvent('break', { detail: { pausedBefore: isPaused } })
);
return !isPaused;
}
continue() {
const isPaused = this.#signalState.isPaused;
if (isPaused) {
this.#signalState.isPaused = false;
}
this.#signal.dispatchEvent(
new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
);
return isPaused;
}
}
... where PausedStateSignal has to extend EventTarget in order to be able of signaling state-changes via dispatchEvent, and where PauseController features the two main methods break and continue.
Both implementations are relying on class syntax, private properties, get syntax and a private, protected state object which gets shared by reference in between a controller and a signal instance. The latter gets achieved by a connecting callback function which is passed at a signal's instantiation time.
Having covered that part, one can continue with the actual implementation of an array method which, in addition of the standard forEach functionality, is capable of three things ...
- allowing to pause/halt the callback function's execution via
break,
- and via
continue allowing ...
- either to continue a paused/halted loop,
- or to skip the loop's next iteration step.
The implementation could be named e.g. forEachAsyncBreakAndContinue; it does make use of the above described signal and controller abstractions, might look like follows ...
function forEachAsyncBreakAndContinue(callback, context = null) {
const { promise, reject, resolve } = Promise.withResolvers();
const controller = new PauseController;
const { signal } = controller;
const arr = this;
const { length } = arr;
let idx = -1;
function continueLooping() {
while(++idx < length) {
if (signal.paused) {
--idx;
break;
}
try {
callback.call(context, arr.at(idx), idx, arr, controller);
} catch (exception) {
reject(exception.message ?? String(exception));
}
}
if (idx >= length) {
resolve({ success: true });
}
}
signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
if (pausedBefore) {
// - continue after already having
// encountered a break-command before.
continueLooping();
} else {
// - continue-command while already running which
// is equal to skipping the next occurring cycle.
++idx;
}
});
continueLooping();
return promise;
}
... and finally gets assigned for demonstration purposes via Reflect.defineProperty as forEachAsyncBC to Array.prototype ...
Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
value: forEachAsyncBreakAndContinue,
});
The now prototypal forEachAsyncBC method is always going to return a promise. This Promise instance either rejects or resolves; the former in case the provided callback function does raise an error at any time it gets invoked, and the latter in case the iteration cycle has been fully completed.
Thanks to all the abstractions an executable example code which does test all of the mentioned features can be written as easy as that ...
(async () => {
const result = await [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
.forEachAsyncBC((value, idx, arr, controller) => {
console.log({ value, idx });
if (value === 9 || value === 3) {
console.log(`... skip over next value => ${ arr[idx + 1] } ...`);
// skip over.
controller.continue();
} else if (value === 4 || value === 6) {
console.log(`... break at value ${ value } ... continue after 5 seconds ...`);
setTimeout(controller.continue.bind(controller), 5000);
// break loop.
controller.break();
}
});
console.log({ result });
})();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
class PausedStateSignal extends EventTarget {
// shared protected/private state.
#state;
constructor(connect) {
super();
this.#state = {
isPaused: false,
};
connect(this, this.#state);
}
get paused() {
return this.#state.isPaused;
}
}
class PauseController {
#signal;
#signalState;
constructor() {
new PausedStateSignal((signal, signalState) => {
this.#signal = signal;
this.#signalState = signalState;
});
this.#signalState.isPaused = false;
}
get signal() {
return this.#signal;
}
break() {
const isPaused = this.#signalState.isPaused;
if (!isPaused) {
this.#signalState.isPaused = true;
}
this.#signal.dispatchEvent(
new CustomEvent('break', { detail: { pausedBefore: isPaused } })
);
return !isPaused;
}
continue() {
const isPaused = this.#signalState.isPaused;
if (isPaused) {
this.#signalState.isPaused = false;
}
this.#signal.dispatchEvent(
new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
);
return isPaused;
}
}
// - asynchronously implemented `forEach` array method which
// provides a `PauseController` instance as 4th parameter
// to its callback function, where the latter's two methods
// `break` and `continue` enable the following ...
//
// - pause a `forEach` loop by invoking `break`.
// - by invoking `continue` ...
// - either continuing a paused `forEach` loop.
// - or skipping the `forEach` loop's next iteration step.
//
function forEachAsyncBreakAndContinue(callback, context = null) {
const { promise, reject, resolve } = Promise.withResolvers();
const controller = new PauseController;
const { signal } = controller;
const arr = this;
const { length } = arr;
let idx = -1;
function continueLooping() {
while(++idx < length) {
if (signal.paused) {
--idx;
break;
}
try {
callback.call(context, arr.at(idx), idx, arr, controller);
} catch (exception) {
reject(exception.message ?? String(exception));
}
}
if (idx >= length) {
resolve({ success: true });
}
}
signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
if (pausedBefore) {
// - continue after already having
// encountered a break-command before.
continueLooping();
} else {
// - continue-command while already running which
// is equal to skipping the next occurring cycle.
++idx;
}
});
continueLooping();
return promise;
}
Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
value: forEachAsyncBreakAndContinue,
});
</script>