>

Ways to use promises - Part 1

_
April 10, 20235 min read

Promise is a fundamental concept of JS (and life). The next quote summarizes it well

"A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason."

ā€” Promises - JavaScript | MDN

Usually, the use of promises is implicit and happens under the hood. I am going to show pitfalls and ways to use it directly or indirectly. This part will cover:

  1. async/await
  2. Generator
  3. AsyncGenerator

async/await

Let's start with the basics:

The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

ā€” async function - JavaScript | MDN

With promises, we write:

function fooPromise() {
  return new Promise((resolve) => resolve(1));
}

And the equivalent async function is:

async function fooAsync() {
  return 1;
}

The results of those functions are the same:

console.log(fooAsync()); // Promise { 1 }
console.log(fooPromise()); // Promise { 1 }
fooPromise().then((result) => console.log(result)); // 1
fooAsync().then((result) => console.log(result)); // 1
console.log(await fooAsync()); // 1
console.log(await fooPromise()); // 1

Some interesting points we see here:

  1. The promises results were evaluated immediately without passing through the pending state.
  2. async always return a promise implicitly.
  3. await is done on promises, it gives the result when it is ready.

What do you say we complicate it a bit? šŸ«£

async function fooAsyncAwait() {
  return await new Promise((resolve) => resolve(1));
}

async function fooAsyncPromise() {
  return new Promise((resolve) => resolve(1));
}

console.log(fooAsyncAwait()); // Promise { <pending> }
console.log(fooAsyncPromise()); // Promise { <pending> }
console.log(await fooAsyncAwait()); // 1
console.log(await fooAsyncPromise()); // 1

In one function we have await and in the other, we don't. Intuitively- await fooAsyncPromise() should have returned a promise, but JS defines a special case for async function that returns a promise.

"Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise."

ā€” async function - JavaScript | MDN

Moreover, we can see that the promise is at pending state and can't be evaluated immediately as before, this is because of how the event-loop work, but that's for another post.

You can await not only promises:

function foo() {
  return 1;
}

console.log(await foo()); // 1
console.log(foo()); // 1

It can be helpful if you don't know if the value is a promise, or not. You can just await it without doing any extra checks.

Generator

In short, it can be described as a way to iterate values one by one instead of storing them in memory.

For example:

function* fooGenerator() {
  yield "a";
  yield "b";
  yield "c";
}

const generator = fooGenerator();
console.log(...generator); // a b c
console.log(...generator); //

You can read more in the MDN docs.

Generators alone are not using promises, but using them in generators is almost natural in many cases. Let's see some code! šŸ‘¾

function* fooGenerator() {
  yield Promise.resolve("a");
  yield Promise.resolve("b");
  yield Promise.resolve("c");
}

console.log(fooGenerator()); // Object [Generator] {}
console.log(...fooGenerator()); // Promise Promise Promise
console.log(await Promise.all(fooGenerator())); // [ 'a', 'b', 'c' ]
for (const x of fooGenerator()) console.log(await x); // a b c
for await (const x of fooGenerator()) console.log(x); // a b c

What can we see here?

  1. Remember that fooGenerator() is a generator of promises, we'll get back to it later.
  2. You can spread the generator, which gives a list of promises, so we can await them using Promise.all.
  3. The for await statement makes an iterable of promises to be sequential. The last two lines are equivalent in that matter.

AsyncGenerator

The difference between Generator and AsyncGenerator can be seen as the difference between a function and async function. The value in the yield statement will be wrapped by promise.

BUT, unlike Generator, the AsyncGenerator can be accessed only by for await or yield*. Array/parameter spreading and destruction will not work!

The for await...of loop and yield* in async generator functions (but not sync generator functions) are the only ways to interact with async iterables. Using for...of, array spreading, etc. on an async iterable that's not also a sync iterable (i.e. it has @@asyncIterator but no @@iterator) will throw a TypeError: x is not iterable.

ā€”Iteration protocols - JavaScript | MDN

Let's take the last example and change the function* to an async function*.

async function* fooAsyncGenerator() {
  yield "a";
  yield "b";
  yield "c";
}

console.log(fooAsyncGenerator()); // Object [AsyncGenerator] {}
console.log(...fooAsyncGenerator()); // Uncaught TypeError: Found non-callable @@iterator
for await (const x of fooAsyncGenerator()) console.log(x); // a b c

There is a very interesting proposal for adding helper functions for the Iterator and AsyncIterator objects. I would love to see that in action!

Conclusion

Promises exist everywhere in JS, directly or indirectly. And you can elaborate many of the JS functionality to easily manage concurrency.

Even with the strong functionality that exists right now, the future of JS is full of additional functionality and optimizations in this subject.

Hope you learned something new about promises, continue reading in part 2.