This content originally appeared on DEV Community and was authored by Arka Chakraborty
When working with TypeScript, one of the most common confusions is around async
functions and their return types. Let’s clear this up with a practical example.
A Practical Example: Checking If a User Is Logged In
Imagine you’re building an app where you need to check if a user is logged in.
Synchronous version
function isUserLoggedIn(): boolean {
return true;
}
This works fine if the answer is always available instantly.
Asynchronous version
But in real-world apps, login status often comes from a server or database:
async function isUserLoggedIn(): Promise<boolean> {
const response = await fetch("/api/check-login");
if (!response.ok) {
return false;
}
const data = await response.json();
return data.loggedIn; // true or false
}
Notice:
- The return type is
Promise<boolean>
(notboolean
). - Even though we return
true
orfalse
, the value is wrapped in aPromise
.
Why Promise and not boolean?
Every async
function in TypeScript always returns a Promise
.
So this:
return true;
is really:
return Promise.resolve(true);
That’s why the return type must be Promise<boolean>
.
If you mistakenly annotate it as just boolean
:
async function isUserLoggedIn(): boolean {
return true;
}
You’ll get a compile error:
Type 'boolean' is not assignable to type 'Promise<boolean>'.
Using the Async Function
// With await
const loggedIn = await isUserLoggedIn();
console.log(loggedIn); // true or false
// Or with .then()
isUserLoggedIn().then(status => console.log(status));
Misconception: Isn’t This Synchronous Because of await?
A common doubt: “If we’re awaiting, aren’t we back to synchronous execution?”
Answer: No.
-
await
pauses execution only in the current function until thePromise
resolves. - But it does not block the event loop. Other tasks can keep running.
So:
-
Async
functions are still asynchronous under the hood. -
await
just gives you synchronous-looking syntax for cleaner code.
Example:
async function main() {
console.log("Before");
await isUserLoggedIn(); // pauses *this* function only
console.log("After");
}
main();
console.log("Outside");
Output order:
Before
Outside
After
Notice how “Outside” runs while we were “waiting.” That’s the power of async/await.
Async Arrow Functions
async
works seamlessly with arrow functions. This is very common in modern TypeScript and React projects.
Example with arrow function
const isUserLoggedIn = async (): Promise<boolean> => {
const response = await fetch("/api/check-login");
const data = await response.json();
return data.loggedIn;
};
In a React handler
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const success = await isUserLoggedIn();
console.log("Login status:", success);
};
With array methods
Arrow functions and async
often pair with array operations:
const urls = ["/a", "/b", "/c"];
const fetchAll = async () => {
const results = await Promise.all(
urls.map(async url => {
const res = await fetch(url);
return res.json();
})
);
console.log(results);
};
Key Takeaways
-
async
functions always returnPromise<T>
, never justT
. - Returning a raw value auto-wraps it in
Promise.resolve(value)
. -
await
pauses locally but doesn’t block the event loop. - Async arrow functions are common in callbacks, handlers, and array methods.
- Use
async
when dealing with APIs, databases, or any future async work — it future-proofs your function signatures.
This content originally appeared on DEV Community and was authored by Arka Chakraborty