How breaking from and continuing with JavaScript’s Array::forEach method actually had to look and to be implemented like.

Peter Seliger
4 min readFeb 26, 2024
AI generated image — Adobe Firefly :: break from and continue with a loop
AI generated image — Adobe Firefly :: break from and continue with a loop

1st things first, despite being discussed and promoted otherwise at experts forums and by online articles, there is not a single clean approach/solution to break from the current execution of a forEach method. Every answer except “throw from within the method’s repetitive invoked callback function” is simply false. Any other claim than the latter just shows a lack of understanding of how array iterating methods are working/implemented; and the throwing approach is as much real bad coding practice as is the mutation of the processed array by setting its length property to zero.

In addition, array methods are neither thought to be interrupted nor to be continued; they implement very specific use cases, of how to handle an array-item during a full array iterating cycle via the provided callback function. This approach, a specialized array-method which invokes a function for each array-item, makes code more readable, better maintainable and enables code reuse since callback functions need to be implemented just once.

Nobody prevents one from continuing using loops like for, for...in, for...of, for await...of, while and do...while. Always pick the tool which fits the use case best. Never blame its wrong usage on the tool.

Thus, since especially at various medium related channels this topic almost always never gets handled correctly, the next provided paragraphs, by explanation and example code, aim to clarify the very matter once and for all.

Note that the intention of this article is not the promotion of utilizing the provided example code. The focus is educational; approach and implemented code are thought to raise the awareness of what’s actually possible to achieve with the currently available language features.

Since the language core not just since lately has much more features than at the start of this millennium, one quite easily can implement 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 e.g.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 });

})();

--

--