Javascript: Disallow for-of

Created on 13 Oct 2016  Â·  113Comments  Â·  Source: airbnb/javascript

The style guide 11.1 says that you shouldn't use for-of and instead should use forEach (which I completely agree with).

That said, the ESLint rule no-iterator doesn't seem to enforce this, and the entry for no-restricted-syntax contains ForInStatement but not ForOfStatement.

Can we add ForOfStatement to no-restricted-syntax (or is the a better way of restricting for-of statements?

pull request wanted guide change

Most helpful comment

@dplusic @ljharb
I don't understand. Where's the advantage in making your code totally unreadable?
Purity is nice, but not when it takes 30 minutes to decipher a simple function.

All 113 comments

Absolutely, thanks! Want to make a PR? it will be semver-major, so i won't be able to merge it immediately, but having it queued up would be great.

Looks like I was beat to the punch by mere hours. Thanks @SimenB.

What should I do in this case:

async function foo(args) {
  for (const arg of args) {
    await bar(arg);
  }
}

@dplusic in nice, simple ES6:

function foo(args) {
  return args.reduce((prev, arg) => prev.then(() => bar(arg), Promise.resolve());
}

It works. Thanks!

@ljharb - what about generators?

I have a function like this that flattens objects recursively, so { user { name: "Lee", location: { city: 'London' } } } becomes {'user.name': 'Lee', 'user.location.city': 'London'}:

function* flatten(obj, str = '') {
  if (obj) {
    for (const name of Object.getOwnPropertyNames(obj)) {
      if (!Array.isArray(obj) && typeof obj[name] === 'object') {
        yield* flatten(obj[name], str ? `${str}.${name}` : name);
      } else {
        yield {
          [`${str}.${name}`]: obj[name],
        };
      }
    }
  }
}

Which is turned into a 'flat' object with Object.assign({}, ...flatten(obj));

I'm probably overlooking an obvious equivalent with map, but a generator solves this quite neatly...

@leebenson generators are not allowed in our guide.

As for an alternative, I'm not sure if this actually works, but perhaps something like:

function flattenObject(obj, prefix = '') {
  return Object.entries(obj).reduce((acc, [key, val]) => {
    const prefixedKey = prefix ? `${prefix}.${key}` : key;
    if (val && !Array.isArray(val) && typeof val === 'object') {
      return Object.assign(acc, flattenObject(val, prefixedKey));
    }
    return Object.assign(acc, { [prefixedKey]: val });
  }, {});
}

Thanks @ljharb. This minor tweak works perfectly:

function flattenObject(obj, prefix = '') {
  return Object.entries(obj).reduce((acc, [key, val]) => {
    const prefixedKey = prefix ? `${prefix}.${key}` : key;
    if (val && !Array.isArray(val) && typeof val === 'object') {
      return Object.assign(acc, flattenObject(val, prefixedKey));
    }
    return Object.assign(acc, { [prefixedKey]: val });
  }, {});
}

@dplusic @ljharb
I don't understand. Where's the advantage in making your code totally unreadable?
Purity is nice, but not when it takes 30 minutes to decipher a simple function.

Of course readability is subjective, but I find loops far more unreadable than a functional alternative.

That's interesting. Of course I don't have any empirical evidence for this, but I bet it'd take the average programer an order of magnitude more time to understand your snippet vs what @dplusic posted.

I hope this fad of functional-at-all-cost is over soon. There's a reason why Haskell hasn't gone mainstream.

I do think @minexew has a point.

With native async/await landing in V8, I think it's going to be pretty common pattern to 'unwind' functional logic like this. for...of and generators often make the code more readable, and easier to follow. I'm not sure effectively banning swaths of valid JS is the way to go.

Personal preference, of course.

i’d actually argue that for..of should be used because of functional style:

.forEach() isn’t functional. it exists for its side effects. so if anything, .forEach() should be banned and for..of allowed in order to make clear that side effects are happening here (which isn’t as apparent from a filter → map → forEach pipeline)

@flying-sheep it's a functional evolutionary step to map and friends. for..of does not lend itself towards evolving to be a proper map, filter, etc. In addition, .forEach forces side effects (or else nothing really happens) - for..of can also be used not for side effects.

behold:

function* map(callable) {
    for (const element of this) {
        yield callable(element)
    }
}
function* filter(predicate) {
    for (const element of this) {
        if (predicate(element)) yield element
    }
}

const odd = '12345'
    ::map(parseFloat)
    ::filter(e => e % 2)

In addition, .forEach forces side effects (or else nothing really happens) - for..of can also be used not for side effects.

both only have side effects (if anything, .forEach returns undefined). what do you mean with “not for side effects”

@flying-sheep lol fair point, both are for side effects since for..of isn't an expression. forget that part.

The :: operator is stage 1, and Airbnb only recommends using stage 3 proposals and above. Similarly, transpiling generators requires regenerator-runtime, which is way too heavyweight to ship to production, so our config also forbids generators at this time.

Similarly, transpiling generators requires regenerator-runtime, which is way too heavyweight to ship to production, so our config also forbids generators at this time.

Kinda tangentially related: would you be interested in a config for server-side only projects? I've used airbnb-base at it works fairly fine, but e.g. trailing commas in functions has to be disabled, and generators could be allowed, etc. There's not a lot of rules that needs tweaking, but maybe you could also include https://github.com/mysticatea/eslint-plugin-node.

If you're interested at all, I can create a new issue for it to discuss its merits there? 😄

Server-side only projects should be using babel also.

Why? With async/await landing, the only syntax I care about is object spread, and it's OK to use Object.assign. Waiting for trailing function commas until support lands isn't an issue either. Adding the overhead of a build step seems unnecessary.

For one, old syntax is generally faster than new syntax, since it's had time to be optimized. Additionally, there'll always be new syntax you want to use, and if your build process is already in place, it's trivial to add that.

A build step shouldn't add overhead, since you should be running your linter and your tests prior to code going to production anyways - just add the build step into there.

@ljharb maybe i didn’t understand you: you argued that it’s an evolutionary step towards functional array methods. i showed that this isn’t the case. my use of generators is irrelevant. e.g. here’s a map implementation without anything you mentioned:

```js
Array.prototype.map = function map(callback) {
const mapped = []
for (const element of this) {
mapped.push(callback(element))
}
return mapped
}

The given implementation of map - or any other abstraction - is entirely irrelevant. forEach is an array prototype method, just like the functional array prototype methods.

I use my babel-preset-node7 for server side use of the airbnb preset. It adds the (very few) missing pieces -- trailing function commas, etc -- and avoids transpiling native features like generators, async/await, etc.

The extra build step makes sense in my case, because I'm writing universal code that targets both the server and the browser, so a build step is necessary anyway. I use generators, async/await, etc freely, and provide a different babel config to each target in webpack. In my use case, adding browser polyfills/runtimes has a negligible effect on the bundle size. My total app is less than 200kb gzipped, with React, Mobx, es2015 polyfills, all app code, etc.

For the most part, I find the airbnb preset to match my own personal stylistic preferences, but I do agree that eliminating for...of is a step too far. I find generators to be very useful for lazy evaluation and for..of (especially coupled with generators and await) makes the code path very easy for others in my team to follow.

In my case, adding a custom rule for no-restricted-syntax fixes my one irritation.

yeah, for..of and generators can be used e.g. for efficient parsers (you delegate to sub parsers via yield*). some languages run on those features extensively, e.g. python and rust.

JS is always the slow kid on the block. maybe in a year or so, we’ll see “map considered harmful” posts that praise how much awesomer generators and comprehensions are.

Comprehensions are never coming to JS, so I doubt you'll ever see that post :-)

Let's see 😁

@flying-sheep see https://twitter.com/esdiscuss/status/590226685020090369, I can assure you there's no "let's see" about it.

that links to this thread, which only links to this protocol, which says:

Conclusion/Resolution

  • Defer comprehensions from ES6 to ES7 to work on the more general type-directed, method-based approach to supporting comprehensions for arbitrary iterable types (arrays, generators, streams, parallel arrays, etc.).

And then after that meeting, https://twitter.com/domenic/status/475371167197302784 → https://twitter.com/littlecalculist/status/475371664041967617 - either way there's no need to debate it further.

OK, but still: generators are here to stay, are very useful, and they work well with for..of syntax.

Whether they're useful is questionable; lazy evaluation providing significant concrete benefits as opposed to theoretical ones is debatable; whether they can be transpiled efficiently is objectively "no". However they are indeed here to say, and do work well with for..of syntax - but this guide will continue to forbid them until there's a transform that does not incur a heavy runtime penalty.

so the scope of this style guide is only the subset of ES6 that can be easily compiled to ES5?

Why? Can’t people choose what targets they want to support? Semi-recent browsers and Node versions do support generators natively

Of course people can choose! We encourage forking of the guide to modify it to suit your purposes. This one is for Airbnb, and as such, we support Airbnb's target browsers.

makes sense 😄

FYI, if you just want to enable for..of in your own .eslintrc(.js), and keep Airbnb's default values for everything else, just put this in the rules section:

'no-restricted-syntax': [
  'error',
  'ForInStatement',
  'LabeledStatement',
  'WithStatement',
]

@leebenson: thanks, I did it a bit more future-proof.


@ljharb: this talk might convince you that for..of is a good idea for the future. In it, the presenter shows several versions of a script, and explicitly mentions how hard to understand the functional, future-based version is compared to the async/await version that uses a for..of loop

await inside a loop can be very very inefficient because it forces serial execution, and most of the time you want concurrent execution.

Lol obviously that's intended

Implementing serial execution with reduce is to me, a bad hack.

Why mimic poorly the behavior of for..of instead of using it? I mean, why you people at TC39 created it in the first place? To have something to blame?

I agree that for..of has its place, but I'm not sure attempting to persuade Airbnb to change their internal style guide is fruitful, or even fair. Their use-case isn't yours/ours. I have 12 variations to the Airbnb base in my .eslintrc.js - far easier/faster to extend _rules_ than to fight/persuade/debate each point on the issue board.

Sure, and as linked above, I've done exactly that.

Maybe I like debating too much.

just want to say that async/await solves a lot of the issues with concurrency and promises on JS. for...of are a must on async functions; maybe it would be wise to consider allowing it inside async functions?

@burabure that's not entirely accurate - await encourages serialization of promise-returning tasks, and most of the time you want concurrency. There's zero reason you need for..of in an async function - you don't even need an await.

What if you need consistent order of execution in promises? Or as i said above, pass values from one promise to another. Hacking with reduce is dirty. And both of this examples need serialization. Sorry if this message seems repetitive, i'm still trying to make my mind on this

It's not "dirty" nor is it "hacking", it's precisely what reduce is for. If you do need serialization, reduce on an array of promise-returning functions is how you should be doing it.

Reduce is for reducing an array -- what would you propose that the reduced version of an array of promise returning functions should be?

@ericblade right. reducing an array - including an array of promise-returning functions. What would you reduce it to? The same thing an async function with await in a loop would - a single Promise.

promiseReturningFunctions.reduce((prev, next) => prev.then(next), Promise.resolve())

If you don't want each next receiving the result of the previous one, then prev.then(() => next()).

which is less readable and takes more time to understand than

for (const url of urls) {
  await download(url)
}

@flying-sheep true, but in that case, making URL downloading serial is just a foolish waste of resources. What you want there is Promise.all(urls.map(download)) so that you let the browser parallelize them to the best of its ability - but thanks for very nicely illustrating the performance hazard that await lends itself to :-)

@ljharb @flying-sheep I think you're confusing the problem that await solves; await is literally waiting for something to happen before you go on with the next line of code, it's purpose is to make these conflated glue code snippets:

somePromise
  .then(x => x)
  .then( x => [SomeExternalThing, x])
  .then((a, b) => moreAsyncStuff(a,b))

into readable sequential code that happens to depend on some asynchronous process.

parallelizing asyncronous proceses is out of the scope and logical context of await by itself.

@burabure yes, I'm aware of that. When you would otherwise have a serial Promise chain, await lets you write that more imperatively. The problem is that the existence of await lures developers into using it when they wouldn't otherwise have a serial Promise chain - and "readable sequential code", while better than "less readable sequential code", is far worse than "less readable concurrent code".

@ljharb agree, but I would argue that in that case the developer is at fault because he didn't know how to use await or haven't read the documentation to understand the basic concept of it.

@burabure features that are useful but sometimes confusing are harmful. Things that are dangerous when developers are ignorant, are dangerous.

By that logic, nobody should use JavaScript in the first place!

@ljharb I would argue that this is much more confusing and harmful

promiseReturningFunctions.reduce((prev, next) => prev.then(next), Promise.resolve())

it's just glue/piping code that says little about the actual work being done.

I think It would be much easier to spot and fix a performance issue on something like

for (const url of urls) {
  await download(url)
}

than debugging an error on this

promiseReturningFunctions.reduce((prev, next) => prev.then(next), Promise.resolve())

I know we're getting waaaaay off topic here, but I don't see how

promiseReturningFunctions.reduce((prev, next) => prev.then(next), Promise.resolve())

is any way less convoluted than any other solution here? (especially given that hardly anyone ever uses the comma operator outside of a for loop, the existence of "Promise.resolve()" without arguments almost appears to be "a feature not a bug")

And in this situation, why wouldn't you just Promise.all() ?

@burabure debugging an error on both is identically easy. debugging performance issues on the former is "the entire use of a loop there is the bug".

@burabure features that are useful but sometimes confusing are harmful. Things that are dangerous when developers are ignorant, are dangerous.

A tad condescending. Eliminating valid syntax based on the idea that other developers are, by default, ignorant to its pitfalls, is making some very unfair assumptions about the 40,000 developers that downloaded your config today, don't you think?

In my experience, for..of with async/await has been an incredible boon to our team, and increased readability throughout our codebase. In the vast majority of our use-cases, a Promise's resolution depends on the results of a previous Promise, for which sequential logic provided by await is far more readable. In the times we need something else, we can explicitly decouple with Promise.all. My code routinely uses variants from both schools to syntax -- I'd imagine it's the same for any experienced JS developer used to the language's quirks (classes, anyone?)

Having a common style guide is one thing, but locking out arguably some of the most interesting parts of JS to land in recent years, is quite another.

Again, no-one's forcing anyone to use this config or abide by every rule, but I do think these kind of hard-line opinions are a bit short-sighted.

I think we should stop comparing for..of and promises here. Promises work great with async-single value stuff, period. Where for..of really shines is when using them for pure and controlled iteration.

And if we all notice, the iterator is something that happens serialized. 1.....2.....3......, in order and one at a time. Just the way for..of works! So why take off for..of if we are working with iterators? They're plain simple and easy to read. When we use promises with things like map and then await using Promise.all(), we are also using the iteration protocol but we are not controlling the iteration (and most times we shouldn't), so it's ok to use promises and chains, but what if we need control over the iteration?

@ljharb You say that reduce is the solution to what i said above, and i kinda agree, because you talk about arrays, but what about other data structures like Map? Map and iterators are very good friends, and it's stupid to do things like [...myMap.entries()].reduce() just because you prefer the promise API (dump an iterator into an array just to iterate the elements again with reduce).

I think there is space for both in the language. Yeah, await leads to serialization where parallelism is probably better, but that's the developer fault. Why for is not in the same place (disallowed)? or classes? They can also suffer from some problems if the developer doesn't do things right.

And lastly, i'm not trying to convince anyone, but i learn so much quality programming with this kind of debates, all points of views contribute something and help creating better software. But we must also admit that this style guide is very much respectable and we can get language features buried just because personal preferences, which could be a mistake. It is ok to not use things, but the rule is excesive in my opinion. And yeah, this is airbnb style guide, fitting airbnb needs :P

You are 100% correct that for non-arraylike iterables, there is no good API to iterate over them. That's a gap in the language I hope to fill one day sooner than later.

However, since this guide prohibits generators, the non-arraylike iterables constitute only Map and Set - and in those cases, indeed, I'd just spread them to an array (note that [...myMap] and [...mySet] are sufficient, because they each have a default iterator) and operate on the resulting array.

I will agree that if we recommended generators in this guide, we'd be in a world where we needed to consume iterables in a generic way, and for..of is currently the best way to do so.

Eventually we have now iterator protocol (after long long lime) in Symbol.iterator
Which is inevitable connected to for .. of

for (let el of document.querySelectorAll('.item')) {
}

In fact every (not only native) non-array iterable needs for .. of
And I guess, that when we have protocol for iteration, more and more libraries will adopt it.

may i remind you of https://github.com/airbnb/javascript/issues/1122#issuecomment-266225868

the sequential downloading is intentional because the API accessed in the talk this code is from rate-limits you if you download in parallel.


what i showed you here is therefore a fully justified use case where forEach can’t be used, reduce is convoluted and hard to immediately grasp, and for..of+await is indubitably the cleanest solution.

i understand that it is the wrong place to discuss this here now, since AirBnB needs to support old browsers and the transformed version of any await based or generator based code performs too badly.

but for the future and a general style guide, for..of is the way to go for some things.

I will agree that if we recommended generators in this guide, we'd be in a world where we needed to consume iterables in a generic way, and for..of is currently the best way to do so.


and i’m happy you see it that way too. maybe we should add a sentence to the motivation for banning for..of that explains it’s only for performance reasons.

PS @ericblade: Promise.resolve() is the second argument to reduce, there is no comma operator in play here. you just confirmed that this variant of the code is more complex than for..of+await.

@flying-sheep by the same token, because your intentions were not apparent and implied a bug, you demonstrated that it is not in fact the cleanest solution.

This is the cleanest solution I can think of:

// download sequentially to avoid rate limiting
for (const url of urls) {
  await download(url)
}

You can’t even use map to create an array of promises and chain them afterwards (chainPromises(urls.map(download))), as they’ll be kicked off as soon as download is called.

So if there’s a cleaner solution for this problem, please tell me, I can’t think of one.

The only functional idea I can come up with is this hideous thing.

At first I thought of defining a chainPromises function for self-documenting code, like chainPromises(urls.map(download)), but as said above, that won’t work. This will, but it’s so ugly:

function chainPromiseFactories (promiseFactories) {
    promiseFactories.reduce((prev, nextFactory) => prev.then(nextFactory()), Promise.resolve())
}

chainPromiseFactories(urls.map(url => () => download(url)))

@flying-sheep since the proper way to handle this is an abstraction that clearly indicates intent, the implementation of that abstraction thus becomes irrelevant. Here's two equivalent approaches:

async function invokeSerially(asyncFunc, args) {
  if (youPreferFunctional) {
    return args.reduce((p, arg) => p.then(() => asyncFunc(arg)), Promise.resolve());
  } else if (youPreferLoops) {
    for (const arg of args) {
      await asyncFunc(arg);
    }
  }
}

invokeSerially(download, urls);

@ljharb To me, for..of indicates intent. The intent of looping an iterator in a serial way (the dev should how for..of works, i think).

What doesn't indicate intent to me is a mix of reduce, resolved promises, await, promise.all and all that stuff together.

Trolling won't be tolerated; let's all be civil to one another.

"the dev should know this" is what I disagree with - anything devs "should" know isn't likely to be a universal thing devs do know - therefore, it objectively can't communicate intent.

@ljharb I wasn't trolling, it was just a (stupid) joke, sorry for that :+1:

@flying-sheep by the same token, because your intentions were not apparent and implied a bug, you demonstrated that it is not in fact the cleanest solution.

The arguments against for..of are getting a little weak, IMO. There are two ways to declare intent - through syntax, or through commentary. I'd argue that @flying-sheep's way is almost _objectively_ cleaner and clearer by syntax alone:

// download sequentially to avoid rate limiting
for (const url of urls) {
  await download(url)
}

It's hard to misinterpret a simple loop, with a single await. There's zero 'parsing' necessary for my brain to do - seeing is understanding. In contrast, this...

args.reduce((p, arg) => p.then(() => asyncFunc(arg)), Promise.resolve());

... requires at least a few second pause to unravel the logic, and try to figure out what's going on. It's like 'goto' statements for my brain. I'm hopping around a fictional interpreter in my head, trying to keep count of what's being thrown back where.

Now, even if you instinctively understand functional logic better, and the latter clicks better with your brain than mine, there are two further points of contention against the syntax:

  1. You're probably a rare/special/gifted developer that inherently likes more complex things. If part of your argument is 'other developers don't universally understand X', I guarantee the same can be applied to your code example!
  2. A one-line comment declaring intent to execute serially makes it crystal clear that the await is intentional.

In any case, there's little need to convolute. Loop + await is _undoubtably_ more straightforward. Even saying those two words out loud declares the _entirety_ of the intent! Not so with 'reduce args by resolving the array item, then running asyncFunc with the next result, then resolve an empty promise (?), ultimately awaiting the whole chain....' Whaaaat? await exists for a reason, and for..of for even more besides. As @felixsanz rightly said (paraphrasing), this isn't necessarily _just_ Promises.

@flying-sheep since the proper way to handle this is an abstraction that clearly indicates intent, the implementation of that abstraction thus becomes irrelevant. Here's two equivalent approaches:

Yes, but the point is, you've _banned_ the latter approach. Conceding that there are two ways to do it, but denying use of the second way, leaves no choice but to follow the convoluted former path.

I mean absolutely zero offence, but I think we have a case of the classic OSS dilemma: a highly opinionated maintainer, and users that disagree with him. You clearly prefer a functional style, and despite some very clear (and I'd say, quite _objective_) arguments, the recent posts have been to the effect of 'well, you could do it both ways...' or 'I think other developers aren't as smart, so we've decided to ban it'.

Both of which are beside the point, and/or not really up to you to nanny. I have no issue with a style guide enforcing consistency (e.g. trailing commas after every argument), but enforcing a blanket ban on perfectly valid - and actually _useful_ - syntax to keep the training wheels on for other developers that might not 'get' that they could be parallelising their async logic, is not a very strong argument for a parent company who's sister projects are machine learning and large-scale deployment. Either developers are 'qualified' to write in JS, or they're not. I don't think that's your concern.

Of course, as I've said before, the easy option for users like myself is to simply fork this style guide and do whatever we want (as, noted, you've already encouraged to do). I also think it's possibly fruitless to try to convince you that for..of is a good thing for _Airbnb_ (I don't think anyone has, btw - this entire thread has about being debating the merits of generic logic for _other_ projects); I get that you want to avoid the use of generator/async-await polyfills as unnecessary bloat in _your_ case, but other developers like me are using this style guide for server-side Node where async/await and generators are baked in and polyfills are irrelevant or a secondary concern.

However, I do think that claiming that loops + await are bad because 'programmers don't get it' isn't serving anyone very well, especially when the alternative is forcing devs down a functional and quite convoluted path that is vastly more complex than the (by definition) simpler/more concise equivalent.

@leebenson You're making a lot of claims about which things are objectively more or less clear that are, quite simply, not objective fact. https://gist.github.com/robotlolita/7643014 and http://qr.ae/RNMEGA are useful reads on the subject.

There's no use discussing this further, since this is Airbnb's style guide, and the for..of construct is banned at Airbnb. Please do fork the guide, or override the appropriate rules, if you do indeed want to use it. This thread is now plenty long enough based on the current state of the language, and generator and async/await transforms, so if folks can't exercise self-restraint from further beating this dead horse, I'll lock the thread.

I read both links, and found nothing to dissuade the use of for..of. The former discusses whether .map is indeed a loop (which further obfuscates the intent of looping/awaiting, if the argument is that it's _not_ a loop); the second makes a case for choosing functional syntax when making a generic loop hides the possibility for side-effects.

The closest paragraph to the central theme of your argument in this thread seems to be:

An imperative loop could be repeating an action a set number of times, traversing or transforming a data structure, waiting for a specific condition, calculating a result step by step
 With higher-order functions, you specify your intent by choosing the appropriate function: if you need to transform every value in a data structure, for example, you use map. Then somebody reading through the code knows exactly what you meant to do at a glance. Similarly, this higher-level approach prevents common bugs—off-by-one errors are impossible with map but oh-so-common with for loops.

Which is perfectly valid, but again, only if choosing a functional paradigm makes it clearer than an equivalent loop. The purpose is at-a-glance code clarity - a .map returns an array, a .forEach loop can have side-effects, .reduce whittles down to a single value, etc. None of these scenarios make...

args.reduce((p, arg) => p.then(() => asyncFunc(arg)), Promise.resolve());

... any clearer than:

// download sequentially to avoid rate limiting
for (const url of urls) {
  await download(url)
}

Agree to disagree, I guess. Thanks for the discourse.

This thread is now plenty long enough based on the current state of the language, and generator and async/await transforms, so if folks can't exercise self-restraint from further beating this dead horse, I'll lock the thread.

On the contrary, I think others stumbling on this thread, who have inherited Airbnb's style guide either on purpose, or as the result of a starter kit of some kind, would find this discussion useful. That's how I wound up here. Suddenly, large swaths of my code were invalid, and I was genuinely curious why.

Again, I've been very clear that the intent is NOT to change Airbnb's style (at least for me; I couldn't care less what Airbnb decides to do with its own code!), but as a point of debate, I would personally disagree with your conclusions and it's very clear others feel the same. By all means, lock - you've made your position quite clear - but I imagine this will be useful to many reading it who are faced with the decision themselves.

On the contrary, I think others stumbling on this thread, who have inherited Airbnb's style guide either on purpose, or as the result of a starter kit of some kind, would find this discussion useful.

yeah exactly. the style guide is very good, open source, well-documented and -motivated.

so people started to depend on it, disregarding the fact that it’s partly written for AirBnB’s specific requirements.

my current line of thought is that the rules which are there because of regenerator’s performance issues (and are therefore without motivation in modern runtimes) should be either

  • clearly marked in the documentation, or
  • extracted to a third package (dependency chain: airbnb-base → airbnb-modern-runtime → airbnb)

the latter is more work, but leads to a package usable for others without AirBnB’s requirements

I'm gonna send my last words here by saying that this is a style guide. To me, restricting language functionality has nothing to do with style, but that's my own interpretation of course.

In my opinion, this should serve as a guide for the community so we end creating software/libraries with the same visual style, so it's easy to read and modify other's code, even inside the same company between colleagues.

It's ok if AirBnB doesn't want to use generators, but what about us? Maybe some of us want to use them, as I said, this is not a matter of style to me. Some people have different needs and use cases.

As @ljharb said:

I will agree that if we recommended generators in this guide, we'd be in a world where we needed to consume iterables in a generic way, and for..of is currently the best way to do so.

So after all this is not a problem with for..of being bad, iteration or serialization. This is just an issue with generators and AirBnB not using them at this moment.

But again, some of us maybe want to use them and restricting the syntax in a guide that should just affect style is a bit weird. What will happen if AirBnB wants to use them in the future? Everyone will have to adapt their code and remove those reduce workarounds?

Final words. AirBnB did a great job creating this style guide and sharing with the community. This repo has almost 45k stars which is huge. Lot of folks are using this style guide. It's popular. And style guides are supposed to create a standard so everyone agree on coding style and we unite as a community, but if AirBnB is so focused on their needs and doesn't "listen" to the community someone is going to make a fork and we will be fragmented as a community again.

Please don't restrict functionality just because your company doesn't use it at this moment. If generators are a bad idea just like abusing the global scope then ok, ban generators and for..of. But if it's not the case, don't do it >.<

Thanks @ljharb for your time, patience and contributions. I'm gonna stop writing here to respect your last post.

All code is style, including architecture choices.

Nothing is restricted because every eslint rule is configurable - go nuts and use whatever you like.

generators and for..of are not something I consider a good idea regardless, but there's no point having that discussion when it's not tenable to use them in production with a reasonable supported browser list. At that time, we can certainly discuss that more fully, and flesh out the guide's description of why to use, or not use, these features.

@dplusic or, in ES2017:

function foo(args) {
  return args.reduce(async (prev, arg) => {
    await prev;
    return bar(arg);
  }, Promise.resolve());
}

@akb89 and without await, since this guide does not yet permit its use:

function foo(args) {
  return args.reduce((prev, arg) => {
    return prev.then(() => bar(arg));
  }, Promise.resolve());
}

@ljharb wrong demo!

function foo(args) {
  return args.reduce((prev, arg) => {
    return prev.then(() => bar(arg);
  }, Promise.resolve());
}

you may forget add ) after bar(arg)

@skyfore thanks, the gist seemed pretty clear either way :-)

@dplusic your example:

async function foo(args) {
    for (const arg of args) {
        await bar(arg);
    }
}

Could be written like this:

async function foo(args) {
    const promises = [...args].map(bar);
    await Promise.all(promises);
}

where all args would be processed in parallel.

@cedevita you are changing the behavior: the first snippet executes the promises sequentially whereas the second one executes them in parallel.

@ljharb here's a use case where your reduce hack will not have the same behavior:

for (const arg of args) {
  try {
    await bar(arg);
  catch (e) {
    return;
  }
}

reduce has no early return so it will loop through the entire args array even if the first bar(arg) call rejects (even though it doesn't execute the remaining bar(arg) since the promise is rejected).

@riwu that has the same conceptual behavior; the fact that it iterates "unnecessarily" through an array that is almost certainly minuscule in size (less than millions of items) doesn't mean the behavior is significantly different.

@ljharb you’re still arguing. what do you think about https://github.com/airbnb/javascript/issues/1122#issuecomment-267580623?

transpiling generators requires regenerator-runtime, which is way too heavyweight to ship to production

@ljharb by "heavyweight" do you mean it increases the bundle size?
I noticed the bundle size increased by 7 KB if I use async/await anywhere.
In that case, why doesn't it warn against the use of async/await in general?

Also, why vanilla for loops do not trigger the same warning? Are they acceptable?

for (let i = 0; i < args.length; i += 1) { // no warning triggered here
  await bar(args[i]);  // a different warning (`no-await-in-loop`) is triggered
}

@riwu bundle size as well as runtime performance. The guide does disallow async/await in general already, because babel-preset-airbnb doesn’t transform it. No, no loops are acceptable, with or without await, we just don’t have the rules configured to warn on for loops yet.

Seriously? this:

 function foo(args) {
  return args.reduce((prev, arg) => prev.then(() => bar(arg), Promise.resolve());
 }

is better than this?

async function foo(args) {
  for (const arg of args) {
    await bar(arg);
  }
}

Did it ever cross your mind that you're too strict and you might even be wrong?
You don't see how the first is much less readable, much more complicated and hacky than the latter? Is all the code in Airbnb hacky one liners?

@SelaO of course, but you're welcome to disagree, and to override any part of the config you like. No, I don't think the latter is more readable - I think the former is the best approach (and in most cases, doing a bunch of async actions in serial is a code smell anyways)

OK. And why does it say here to prefer for-of but I get this with a for-of?

2018-05-15_10-48-36

@SelaO That says "Prefer JavaScript’s higher-order functions" instead of "loops like for-in or for-of".

Right, but [].forEach() is allowed? How is that better than for-of?

forEach is an iteration, not a loop.

for-of doesn't iterate over all the elements (just like for each but without an unnecessary lambda)?

And if loops are so bad, then why do you allow regular loops then like for(let i = 0; i<x; i++){}?

We don’t yet lint against for loops because we still have them in a number of places. We plan to lint against them at some point too, but for now the general guide advice to avoid all loops is sufficient.

What is the rule to allow for-of? I can't find it.
It looks like no-restricted-syntax is too broad.

Why are you fighting Airbnb on their _own_ styleguide? If you don't agree with it, extend it. Or go make your own.

I personally use for...of extensively and find it goes hand-in-hand with async/await. I've covered the reasons at length above. But forcing Airbnb to change their _own_ rules to suit _your_ preferences is ludicrous.

I'm not forcing anyone, I just try to understand or find a good reason for these rules.

My point is: just extend it and make it your own. Everyone has their own (strong) opinions so trying to change them is pointless.

FWIW, I completely agree that for/of is useful and functional-at-any-cost is overkill. Just do whatever works for your code.

Why are you fighting Airbnb on their own styleguide? If you don't agree with it, extend it. Or go make your own.

i think your answer is in https://github.com/airbnb/javascript/issues/1122#issuecomment-267580623

i think your answer is in #1122 (comment)

Right, so everything there is to be said, has already been said. Conclusion: Extend/make your own.

Just found this thread. Hope I'm not too late to ask questions.
Say I have a function

someFunc = (array) => {
    for (const item of array) {
       if (someCondition) return false;
    }
    return true;
}

What's the equivalent solution if not using for-of loop ?

@fleksin
Assuming someCondition can better be represented as somePredicate(item), the existing logic gets true if "every item produces false", so:

someFunc = array => array.every(item => !somePredicate(item));

Alternatively, if "any item produces true":

someFunc = array => array.some(item => somePredicate(item));

but you'll want to confirm that the behavior matches your expectations for an empty array.

Just replace for of by .forEach

REPLACE this

async function foo(args) {
  for (const arg of args) {
    await bar(arg);
  }
}

BY this

async function foo(args) {
args.forEach((arg) => {
    await bar(arg);
  }
}

and no need to this weird code

function foo(args) {
  return args.reduce((prev, arg) => prev.then(() => bar(arg), Promise.resolve());
}

@hmagdy No, they are not same.
The former executes the promises sequentially, while the latter executes them at them same time.

Indeed, you'd need the (not weird - a normal part of JS and functional programming) reduce instead.

The former executes the promises sequentially, while the latter executes them at them same time.

The former actually results in a “SyntaxError: await is only valid in async function” as expected.

The arrow function isn’t async, and await needs to be in an async function directly. If we make it async, you’re of course right, but that’s not a surprise: Just calling async functions (as forEach does) runs them parallely.


I don’t think this discussion is helpful anymore. Everything’s been said on why the rule is there:

  • The AirBnB JS style is tailored for AirBnB’s use
  • AirBnB targets older browsers without iterator support: https://github.com/airbnb/javascript/issues/1122#issuecomment-265841379
  • The AirBnB guide recommends forEach because for..of relies on iterators, and polyfill support for old browsers here is slow.
  • It’s easy to create your own derived style guide that is tailored to your preferences.

I think the only thing left to discuss is what I said in https://github.com/airbnb/javascript/issues/1122#issuecomment-267580623, and I filed #2019 for that.

To clarify; we recommend forEach because loops should basically always be avoided.

People don’t agree with you here, see the discussion in #2019

@dplusic in nice, simple ES6:

function foo(args) {
  return args.reduce((prev, arg) => prev.then(() => bar(arg), Promise.resolve());
}

That's not simple neither nice neither readable compared to its for...of counterpart tho 😂

@MelMacaluso i don't agree; but obviously it's subjective.

for of 👍

I want to process an iterator. Is there a functional way of doing this?

let result = iterator.next();
  while (!result.done) {
    console.log(result.value);
    result = iterator.next();
  }

@Ghost---Shadow Array.from(iterator, item => process(item))? There's also https://npmjs.com/iterate-value / https://npmjs.com/iterate-iterator if that's useful for you.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

molily picture molily  Â·  37Comments

PandaWhisperer picture PandaWhisperer  Â·  44Comments

AugustinLF picture AugustinLF  Â·  30Comments

ljharb picture ljharb  Â·  139Comments

SimenB picture SimenB  Â·  34Comments