Im pretty new to using the await
keyword, I am used to the older "Promise" like commands used in Cypress.
However one confusion I have is when a function returns a promise, but there is both an await
keyword IN the function and when using the function. For example:
async goto() {
await this.page.goto('https://playwright.dev');
}
Above is a simple method used in Page Object Model pattern, page.goto
returns a promise, so we await
it.....which makes sense, but to use that you also have to await
via:
await playwrightDev.goto();
So I understand the syntax, because the goto
function is async
. However I guess I don't really understand "why" we have to do this. More specifically why the function has to be async
. Because the inner command of the function is already waiting for a promise, why does the function itself need to be async. Since it won't return until the innermost command is done anyways?
Hopefully what I am asking makes sense. I understand the syntax but not WHY it is like this.
A common beginner misconception is that once you await
a promise, you can somehow go back to your mainline synchronous code. On the contrary, once you have a promise, you're stuck in asynchronous mode for any other code that depends on that result, regardless of whether the dependent code is asynchronous or not.
That's not to say you can't have runs of synchronous code along the way, only that any functions that consume a promise themselves basically become promises, transitively.
As a rule of thumb, there's 1 await
per promise, with the caveat that any callers of asynchronous code that use await
now return promises themselves.
You don't have to use async
/await
inside the method since there's only a single promise at hand. You can return the promise that page.goto
returns, passing it up to the caller and essentially stripping off a superfluous promise wrapper:
goto() {
return this.page.goto('https://playwright.dev');
}
Either way, the caller will need to await
the returned promise, unless they don't care when the navigation happens and don't need the resolved value from the promise, which is usually not the case.
To track your promises, keep your promise resolutions together in the same chain(s), occurring sequentially. Every single promise in the chain needs to be await
ed or chained with then
. As soon as a single promise is not await
ed, the chain breaks and the second part of the chain has no way of getting values from or awaiting completion of the first part.
The reason that calling code is "polluted" by promises has to do with the fact that promises are just syntactic sugar for asynchronous callbacks (think setTimeout
), and callbacks don't run in mainline code. All synchronous execution ends before any promises run. Then, when the promises resolve later on, execution resumes, running the "callback" (or code after await
or in the next .then
handler).
async
/await
just makes it appear like you're doing it all synchronously in a function, but that's just a syntactical trick, flattening out the callbacks.
If you could use await
, then resume mainline synchronous code, it would be a non-asynchronous, blocking call that ties up the single Node thread (imagine the Node version of "this page is not responding" that you may see in the browser when too much blocking CPU-bound processing occurs and the event loop doesn't get a chance to run and repaint the screen or handle an interaction).