Better Promises in JavaScript
francisco.io
francisco.io
Example:
const lines = (await fetch(myUrl)).text().split('\n');
[1] http://vanilla-js.com/Edit: magic-promises promotes all accessed properties to magic-promises as well, and performs magic on functions, so it's not exactly the same, but I'm not sure if I'd want to add the proxy overhead to my code just to make my code more "fluent".
const lines = (await (await fetch(myUrl)).text()).split('\n'); function fetchText(input?: string | Request, init?: RequestInit) {
return fetch(input, init).then(x => x.text())
}
function fetchJson(input?: string | Request, init?: RequestInit) {
return fetch(input, init).then(x => x.json())
}With magic-promises you don't need to worry about whether something regurns a promise or not. See fs-array, some methods will return promises and some won't, but you can just treat them all the same.
I disagree. Explicit is better than implicit.
I'm less fan of the second proposition. Imagine someone seeing that snippet on internet and copying it: he would have no way of knowing that ".text()" is actually a promise and could potentially be making more I/O calls than he thought.
How long since someone will figure out how to get rid of the (error-prone) await keyword, and go back full circle on plain old synchronous-style syntax?
const lines = await fetch(...).then(res => res.text()).then(res => res.split('\n'))
Any use of .then in async/await is a code smell.The obvious single line refactor (as presented in other threads):
const lines = (await (await fetch(url)).text()).split('\n')
But a clearer refactor is simply multiple lines to make each "step" clear as mud: const response = await fetch(url)
const text = await response.text()
const lines = text.split('\n')
Yes, it's a bit over-verbose than the one liner, but ergonomically it is step-by-step imperative programming just like Grandma C++ used to bake, and few would argue that it isn't readable old-fashioned ergonomics.Promise.all is a tougher thing to get "ergonomics" from, but more often than not, a refactor to a "classic" for loop pattern is often clearer. The semantics change, unfortunately, yet more often than not the clearer "one-thing-at-a-time" of the simpler refactor is easier to debug, less harsh on called services, less pressure on client memory, and simpler to progress report.
Instead of:
const responses = await Promise.all(urls.map(url => fetch(url)) const texts = await Promise.all(responses.map(response => response.text()) for (let text of texts) { doSomething(text) }
Try:
for (let url of urls) {
const response = await fetch(url)
const text = await response.text()
doSomething(text)
}
Again, the semantics change: instead of waiting for all of them to complete as massively parallel it operates a bit more "synchronously" one at a time. But the benefits again are that ergonomically it is much clearer what each individual step is, how much work has been done, roughly how much work is left to do. Additionally, you aren't creating a massive array of the text of every response before operating on each text, which can be a big deal for performance memory pressure. More often than not, I've seen cases where the simpler for (let of)/await pattern here is more performant than all the parallel calls simply because of the drop in this memory pressure.For cases where you do need more of the parallelism, ESNext AsyncIterable is a better plan than Promise.all(), and affords the for-await pattern:
for await (let response of responsePromiseIterator) {
const text = await response.text()
doSomething(text)
}
Which again, is more ergonomic overall, often easier to get the performance right than Promise.all(), and easy enough to change to different parallel strategies because the code stays the same regardless of the scheduler used to build your async iterator.There is also this minor thing that that the compiler is not written in PureScript.