The moment a Promise object is created typically coincides with when the async
request is made. Think about using fetch or setTimeout. For example, the
delay function below using setTimeout starts the async work the instant the
Promise is created.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
delay(1000); // Starts counting 1 second immediately, regardless of await
But with Supabase, no async request is made until you await. This lets you
conditionally build a query and only fire the request once it’s complete.
// No request yet
let query = supabase.from("...").select("...");
if (tagIds) query = query.in("id", tagIds);
// Request happens here
const { data, error } = await query;
Another peculiarity: even if you await multiple times, each await sends a
fresh request matching the current query state.
let query = supabase.from('...').select('...');
// Executes query with select only
let { data, error } = await query;
query = query.order('created_at', { ascending: true });
// Executes query with select + order
{ data, error } = await query;
The types returned by select or order are custom objects like
PostgrestTransformBuilder and PostgrestFilterBuilder, not Promises. It’s
clear they’re using the
builder pattern for
step-by-step query construction, but how can you await an object that isn’t a
Promise?
Looking at the
Supabase source code,
both PostgrestTransformBuilder and PostgrestFilterBuilder extend
PostgrestBuilder, which implements PromiseLike.
export default abstract class PostgrestBuilder<
Result,
ThrowOnError extends boolean = false,
> implements PromiseLike<
ThrowOnError extends true
? PostgrestResponseSuccess<Result>
: PostgrestSingleResponse<Result>
> {
// ...
}
In the
TypeScript source code,
PromiseLike is defined as an object that implements a then method.
interface PromiseLike<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| undefined
| null,
): PromiseLike<TResult1 | TResult2>;
}
Since PostgrestBuilder implements PromiseLike, it must have a then method.
Let’s take a look.
export default abstract class PostgrestBuilder {
...
then(onfulfilled?, onrejected?): PromiseLike<TResult1 | TResult2> {
...
const _fetch = this.fetch;
let res = _fetch(this.url.toString(), {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.body),
signal: this.signal,
}).then(async (res) => {
...
});
...
return res.then(onfulfilled, onrejected);
}
}
The actual network request happens inside the then method.
We should let go of the assumption that everything you await must be a Promise
object. The following code works perfectly fine:
const obj = {
then(resolve, reject) {
resolve("hello");
},
};
async function test() {
const res = await obj;
console.log(res);
}
test();
MDN’s Promise documentation has a relevant note:
The JavaScript ecosystem had made multiple Promise implementations long before it became part of the language. Despite being represented differently internally, at the minimum, all Promise-like objects implement the Thenable interface. A thenable implements the .then() method, which is called with two callbacks: one for when the promise is fulfilled, one for when it’s rejected. Promises are thenables as well.
To interoperate with the existing Promise implementations, the language allows using thenables in place of promises.
Apparently a popular interview question
Is there a reason to use React beyond job hunting?