Introduction
If you've worked with async/await in JavaScript or TypeScript, you might have encountered a common question: Why can you return a plain value from an async function even though the return type is Promise<T>?
This is a great question that trips up many developers! Let me explain how async functions automatically handle return values.
The Question
Why can you write this:
const syncToServer = async (): Promise<{ success: boolean }> => {
if (itemsToSync.length === 0) {
return { success: true }; // ← Just a plain object, not a Promise!
}
// ...
}
Instead of this:
const syncToServer = async (): Promise<{ success: boolean }> => {
if (itemsToSync.length === 0) {
return Promise.resolve({ success: true }); // ← Wrapped in Promise
}
// ...
}
The Answer: async Functions Auto-Wrap Return Values
When you declare a function with async, JavaScript automatically wraps any return value in a Promise. You don't need to manually create a Promise.
How It Works
When you write:
const myFunction = async () => {
return { data: "hello" };
}
JavaScript automatically converts it to:
const myFunction = () => {
return Promise.resolve({ data: "hello" });
}
Code Examples
Example 1: Simple Return Value
// ✅ This works perfectly
const fetchData = async (): Promise<string> => {
return "Hello, World!"; // Automatically wrapped in Promise.resolve()
}
// Usage
fetchData().then(result => console.log(result)); // "Hello, World!"
Example 2: Conditional Returns
const syncToServer = async (): Promise<{ success: boolean; errorMsg?: string }> => {
const itemsToSync = getUnsyncedItems();
if (itemsToSync.length === 0) {
return { success: true }; // ✅ Auto-wrapped in Promise
}
try {
await sendToServer(itemsToSync);
return { success: true }; // ✅ Also auto-wrapped
} catch (error) {
return { success: false, errorMsg: error.message }; // ✅ Also auto-wrapped
}
}
Example 3: All These Are Equivalent
// Option 1: Using async (RECOMMENDED - cleanest)
const getValue = async (): Promise<number> => {
return 42;
}
// Option 2: Manually wrapping (works, but redundant)
const getValue = async (): Promise<number> => {
return Promise.resolve(42); // Unnecessary when using async
}
// Option 3: Without async, must manually create Promise
const getValue = (): Promise<number> => {
return Promise.resolve(42); // Required if not using async
}
Example 4: Error Handling
const fetchUser = async (id: string): Promise<User> => {
if (!id) {
throw new Error("ID is required"); // ✅ Auto-wrapped in Promise.reject()
}
const user = await database.getUser(id);
return user; // ✅ Auto-wrapped in Promise.resolve()
}
// Usage
fetchUser("123")
.then(user => console.log(user))
.catch(error => console.error(error)); // Handles the thrown error
Key Rules
-
asyncfunctions always return a Promise - even if you return a plain value -
Returning a value = automatically wrapped in
Promise.resolve(value) -
Throwing an error = automatically wrapped in
Promise.reject(error) - You can still return a Promise explicitly, but it's unnecessary
When You DO Need new Promise()
You only need new Promise() when you're wrapping non-promise-based async code (like callbacks):
// Wrapping a callback-based API
const delay = (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms); // Callback → Promise
});
}
// But with async/await, you can simplify:
const delay = async (ms: number): Promise<void> => {
await new Promise(resolve => setTimeout(resolve, ms));
}
Tags: #javascript #typescript #async-await #promises #webdev #programming
Top comments (0)