How to Return a Response from an Asynchronous Call: A Complete Guide for JS

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

"I run a function, and it returns undefined!" — A familiar pain?

\n

Imagine: you're writing JavaScript code, making an API request, waiting for data… and in response you get undefined. Or worse — Promise {<pending>}. You feel like giving up, closing the browser, and becoming a monk.

\n

Don't rush. This problem plagues even experienced developers. It's no wonder the question "How do I return the response from an asynchronous call?" on Stack Overflow has gathered thousands of views and hundreds of answers. Today we'll break it down once and for all.

\n

In this article you will learn:

\n
    \n
  • Why return doesn't work the way you're used to
  • \n
  • How to tame asynchronicity with callbacks, promises, and async/await
  • \n
  • Which approach to choose for your project
  • \n
\n

Let's go!

\n\n

Why doesn't return return the data?

\n

Let's get straight to the point. Here's a classic example:

\n\n
function getUserData(userId) {\n  fetch(`https://api.example.com/users/${userId}`)\n    .then(response => response.json())\n    .then(data => {\n      return data; // Seems fine, right?\n    });\n}\n\nconst user = getUserData(42);\nconsole.log(user); // undefined
\n\n

Familiar? You wrote return data inside .then(), but outside you got undefined. Why?

\n

JavaScript is single-threaded and non-blocking. When you call fetch, it doesn't wait for the server's response. Instead, it "defers" the callback execution to the side and immediately moves to the next line. And the next line is console.log(user), where user is not yet defined.

\n

Your getUserData function finishes before the server responds. It returns nothing (returns undefined), and the data is left hanging in the air.

\n

How to fix this? There are three main ways.

\n\n

Method 1: Callbacks — the grandfather of asynchronicity

\n

Back in the day, before promises existed, all asynchronous operations were done using callbacks. You pass a function that will be called when the data arrives.

\n\n
function getUserData(userId, callback) {\n  fetch(`https://api.example.com/users/${userId}`)\n    .then(response => response.json())\n    .then(data => {\n      callback(data); // Call the callback with data\n    })\n    .catch(error => {\n      callback(null, error); // Or pass the error\n    });\n}\n\n// Usage:\ngetUserData(42, (data, error) => {\n  if (error) {\n    console.error('Error:', error);\n    return;\n  }\n  console.log('User data:', data);\n});
\n\n

Pros: simplicity, works everywhere.

\n

Cons: if you nest several callbacks inside each other, you get "callback hell". The code becomes unreadable.

\n\n
getUserData(42, (data1) => {\n  getPosts(data1.id, (data2) => {\n    getComments(data2[0].id, (data3) => {\n      // ... and so on\n    });\n  });\n});
\n\n

Modern JavaScript has moved away from this. But understanding callbacks is necessary — they are the foundation of all other approaches.

\n\n

Method 2: Promises — saviors of readability

\n

A promise is an object that represents a "promise" to return a value in the future. It has three states: pending, fulfilled, and rejected.

\n

Let's go back to our example. Instead of a callback, we return the promise itself:

\n\n
function getUserData(userId) {\n  return fetch(`https://api.example.com/users/${userId}`)\n    .then(response => {\n      if (!response.ok) {\n        throw new Error('Network is not okay');\n      }\n      return response.json();\n    });\n}\n\n// Usage:\nconst userPromise = getUserData(42);\n\nuserPromise\n  .then(data => {\n    console.log('User data:', data);\n  })\n                

Blogs

Book Recommendations