Introduction
When an async function returns undefined despite using await, the cause is almost always a missing return statement, calling the function without await, or await-ing something that is not a Promise. Every async function wraps its return value in a Promise — if there is no explicit return, the Promise resolves to undefined. This article covers the most common causes and fixes for the "async/await still returns undefined" problem.
Cause 1: Missing return Statement
1// WRONG: no return — resolves to undefined
2async function getUser(id) {
3 const response = await fetch(`/api/users/${id}`);
4 const data = await response.json();
5 // Missing: return data;
6}
7
8const user = await getUser(1);
9console.log(user); // undefined
10
11// FIX: add the return
12async function getUser(id) {
13 const response = await fetch(`/api/users/${id}`);
14 const data = await response.json();
15 return data; // Now resolves to the user object
16}
If the function body ends without a return, JavaScript implicitly returns undefined, and the Promise resolves to undefined.
Cause 2: Not Awaiting the Async Function
1async function fetchData() {
2 const res = await fetch("/api/data");
3 return res.json();
4}
5
6// WRONG: not awaiting — result is a Promise, not the data
7const result = fetchData();
8console.log(result); // Promise { <pending> }
9
10// FIX: await the call
11const result = await fetchData();
12console.log(result); // { ... actual data }
13
14// Or use .then()
15fetchData().then(data => console.log(data));
async functions always return a Promise. Without await or .then(), you get the Promise object, not its resolved value.
Cause 3: Returning Inside a Callback
1// WRONG: return inside forEach does nothing for the outer function
2async function findUser(users, targetId) {
3 users.forEach(user => {
4 if (user.id === targetId) {
5 return user; // Returns from the forEach callback, NOT from findUser
6 }
7 });
8 // Implicitly returns undefined
9}
10
11// FIX: use a regular loop
12async function findUser(users, targetId) {
13 for (const user of users) {
14 if (user.id === targetId) {
15 return user; // Returns from findUser
16 }
17 }
18 return null;
19}
20
21// Or use Array.find
22async function findUser(users, targetId) {
23 return users.find(user => user.id === targetId) || null;
24}
Cause 4: Awaiting a Non-Promise Value
1// This works — await on non-Promise just returns the value
2async function example() {
3 const value = await 42;
4 return value; // 42
5}
6
7// But this is a problem — sync function that doesn't return a Promise
8function getData() {
9 fetch("/api/data").then(res => res.json());
10 // Missing return — returns undefined synchronously
11}
12
13async function main() {
14 const data = await getData();
15 console.log(data); // undefined — getData() returned undefined, not a Promise
16}
17
18// FIX: return the Promise chain
19function getData() {
20 return fetch("/api/data").then(res => res.json());
21}
Cause 5: Conditional Return Paths
1// WRONG: only returns in the if branch
2async function getPrice(productId) {
3 const res = await fetch(`/api/products/${productId}`);
4 if (res.ok) {
5 const product = await res.json();
6 return product.price;
7 }
8 // Missing return for error case — returns undefined
9}
10
11// FIX: handle all paths
12async function getPrice(productId) {
13 const res = await fetch(`/api/products/${productId}`);
14 if (!res.ok) {
15 throw new Error(`HTTP ${res.status}`);
16 }
17 const product = await res.json();
18 return product.price;
19}
Cause 6: Using async with .map() or .forEach()
1// WRONG: async forEach doesn't wait for completion
2async function processAll(items) {
3 items.forEach(async (item) => {
4 await processItem(item);
5 });
6 // Returns immediately — items not yet processed
7 console.log("Done"); // Logs before processing finishes
8}
9
10// FIX: use for...of for sequential processing
11async function processAll(items) {
12 for (const item of items) {
13 await processItem(item);
14 }
15 console.log("Done"); // Logs after all items are processed
16}
17
18// FIX: use Promise.all for parallel processing
19async function processAll(items) {
20 const results = await Promise.all(items.map(item => processItem(item)));
21 console.log("Done", results);
22}
Debugging Async Undefined
1async function debugExample() {
2 const step1 = await fetchStep1();
3 console.log("Step 1:", step1); // Check each step
4
5 const step2 = await fetchStep2(step1);
6 console.log("Step 2:", step2); // Find where undefined appears
7
8 return step2;
9}
10
11// Wrap in try/catch to see errors
12async function safeFetch() {
13 try {
14 const res = await fetch("/api/data");
15 const data = await res.json();
16 return data;
17 } catch (error) {
18 console.error("Fetch failed:", error);
19 return null; // Explicit return on error path
20 }
21}
Common Pitfalls
Forgetting return in the async function body: The most common cause. An async function without a return statement resolves its Promise to undefined. Always explicitly return the value you want.
Using forEach with async callbacks: forEach does not await async callbacks — it fires them all immediately and returns. Use for...of for sequential execution or Promise.all(arr.map(...)) for parallel.
Calling an async function without await: const result = asyncFn() assigns a Promise, not the resolved value. Either await the call or use .then() to access the result.
Returning inside a nested callback: return inside a .forEach(), .map(), or .filter() callback returns from that callback, not from the enclosing async function. Use for...of loops or Array.find() instead.
Not handling all conditional branches: If only the if branch returns a value and the else branch is missing, the function returns undefined when the condition is false. Ensure every code path has an explicit return or throws an error.
Summary
Always include a return statement in async functions — no return means the Promise resolves to undefined
Always await async function calls (or use .then()) — without it you get a Promise, not the value
Do not use async callbacks with forEach — use for...of or Promise.all(items.map(...))
Check all conditional branches return a value or throw an error
Log intermediate values to find where undefined first appears in the chain
Use try/catch with explicit returns on error paths to avoid silent failures