1

How can I break (similar to the break statement) from an implicit loop on an array?

The Array.prototype.map, Array.prototype.forEach, etc. functions imply a loop over the elements of the array. I want to conditionally break that loop early.

This contrived example:

const colours = ["red", "orange", "yellow", "green", "blue", "violet"];

colours.map(item => {
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        break;
    }
});

causes a SyntaxError: unlabeled break must be inside loop or switch.

How can I break the loop the same way a break statement would?

bignose
  • 30,281
  • 14
  • 77
  • 110
  • 1
    `Array#map` is not designed for iteration with side effects (and must not be used for that purpose), it cannot be stopped. `Array#forEach` cannot be stopped either though. If you need to find something you need to use `Array#indexOf` or `Array#find` or any other lookup method. – zerkms May 28 '18 at 02:08
  • You can't break a map. You'll have to use a stoppable method, such as `for` or `while` or, as @zerkms suggested, you could search for your item without iterating at all. – tao May 28 '18 at 02:09
  • You can't because those methods don't allow for it. But, if you really need that functionality, you should just use a `for` loop with a counter. – Scott Marcus May 28 '18 at 02:25

5 Answers5

4

You can't do it using a regular way. You can emulate the break behaviour by remembering whether the loop is "broken". The lack of this solution is that the loop actually continues (though the iteration logic is skipped).

let isBroken = false;

colours.map(item => {
    if (isBroken) {
        return;
    }
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        isBroken = true;
        return;
    }
});

The best solution for your example is to use a plain for loop.

for (colour of colours) {
    if (colour.startsWith("y")) {
        console.log("The yessiest colour!");
        break;
    }
}

Also you can use a dirty way to actually stop a map loop.

colours.map((item, index, array) => {
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        array.splice(0, index);
    }
});
// The colours array will be modified after this loop
Finesse
  • 9,793
  • 7
  • 62
  • 92
3

Although forEach is designed to run some function that doesn't change the array (i.e. it is designed to do some other side effect for each item), it is explicitly documented to have no way of breaking the loop.

From the MDN documentation for forEach:

There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.

So, despite forEach being designed for side-effects, there is no normal access to the control structure of the loop.

Because Array.prototype.map and Array.prototype.reduce are intended to generate a new value, they are not designed for side effects like breaking early. The documentation doesn't appear to say that explicitly.


Possible alternatives to try: re-write the code to use Array.prototype.some or Array.prototype.every. These are explicitly documented to terminate the loop early when their condition is known (when some would return true, or when every would return false).

const colours = ["red", "orange", "yellow", "green", "blue", "violet"];

colours.some(item => {
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        return true;
    }
});
bignose
  • 30,281
  • 14
  • 77
  • 110
  • What exactly do you mean by *"`forEach` being designed for side-effects"*? – tao May 28 '18 at 02:23
  • Not sure why this was down voted. It's correct and it explains why very well. – Scott Marcus May 28 '18 at 02:26
  • @Scott, basically because the question was asked without research and when OP finally researched decided to answer their own question with what they should have read before asking. – tao May 28 '18 at 02:29
  • @AndreiGheorghiu, answering one's own question is a valid way of using StackExchange sites. I had this problem, and wanted to document the answer once I found it. – bignose May 28 '18 at 02:30
  • I personally won't. Duplicating MDN content on StackOverflow won't get my thumb up. Here's [my opinion](https://meta.stackoverflow.com/questions/261592/how-much-research-effort-is-expected-of-stack-overflow-users) on the subject. And by the way, what you linked is not "documentation for `forEach`". [This one](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.foreach) is. – tao May 28 '18 at 02:37
  • Also, `Array#reduce` is not intended to generate a new array - it can be used for that, but not exclusively and in most of such cases `Array#map`, `Array#filter` is more appropriate. – wiesion May 28 '18 at 03:41
1

you can throw an exception if your only option is to use Array.forEach

Refer this:

How to short circuit Array.forEach like calling break?

There are other methods available which can solve your purpose too. For example you can use method: Array.prototype.some() if you want to check some condition and break the loop based on that condition.

Example can be referenced here:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some

Bhavya Bansal
  • 257
  • 1
  • 15
1

Array#map, Array#forEach and so on have never been designed to be stopped. That would feel odd since the intent of map as well forEach really is to iterate over all items.

Also i don't think it is possible to notify the caller that a break event has occurred, since it is within a function that is not an integral part of the original loop.

So let's see at a custom method that stops the loop at the first occurrence of true without returning the matching value itself:

Object.defineProperty(Array.prototype, 'untilTrue', {
    enumerable: false,
    value: function(lambda) { 
     for(let i in this) {
       if(lambda.call(this, this[i])) return;
      }
    }
});

const colours = ["red", "orange", "yellow", "green", "blue", "violet"];

colours.untilTrue(item => {
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        return true;
    }
    console.log(item);
});

Comparing this custom untilTrue to the use of Array#find:

const colours = ["red", "orange", "yellow", "green", "blue", "violet"];

colours.find(item => {
    if (item.startsWith("y")) {
        console.log("The yessiest colour!");
        return true;
    }
    return false;
});

The only notable difference is that untilTrue doesn't return the matching item - Array#find does that in addition to call lambda.

So in general i would just stick to Array#find to keep the code neat and clean and use it like this:

const colours = ["red", "orange", "yellow", "green", "blue", "violet"];

if(colours.find(item => item.startsWith("y")) !== undefined) {
  console.log("The yessiest colour!");
}

This stops the loop at the first match (and returns the matching element). Also note that you have to compare against undefined - in case you were searching for a false or null value, the check would never evaluate to true if just compared to true.

bignose
  • 30,281
  • 14
  • 77
  • 110
wiesion
  • 2,349
  • 12
  • 21
0

you can create your custom forEach method in these ways

Array.prototype._forEach = function (callback) {
  let _break = false;

  const breaker = () => {
    _break = true;
  };

  for (let index = 0; index < this.length; index++) {
    if (_break) break;

    callback(this[index], index, breaker);
  }
};


// Example for usage:

const letters = ["a", "b", "c", "d", "e", "f", "g"];

letters._forEach((data, index, breaker) => {
  if (data === "c") return; // continue role

  if (data === "e") return breaker(); // break role

  console.log(`log ${index}:  ${data}`);
});

/**
 * result:
 *  log 0:  a
 *  log 1:  b
 *  log 3:  d
 */

or you can create top custom forEach method by creating

function forEach(items, callback) {
  let _break = false;

  const breaker = () => {
    _break = true;
  };

  for (let index = 0; index < items.length; index++) {
    if (_break) break;

    callback(items[index], index, breaker);
  }
}