Promise pending when JSON has been returned

I feel like I’m misunderstanding something fundamental about the JavaScript fetch API.

My ultimate goal here is to make a call to my PHP, get a response in JSON, and then do something in JavaScript when that response comes back. But I can’t get even a very stripped-down version to behave as I would expect. I’ve been looking at tutorials for hours, but I seem to be missing something crucial.

Here’s my index.php file, which generates two buttons:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
	</head>
	<button class="btn">Test Button 1</button>
	<button class="btn">Test Button 2</button>
	<script src="test.js"></script>
	</body>
</html>

This is the test.js file:

const btns = document.querySelectorAll('.btn');

function btnFunc() {
	output = asyncFunc()
		.then(response => console.log(response));
	console.log(output);
}

async function asyncFunc() {
	const response = await fetch('/target.php');
	return response.json();
}

btns.forEach( btn => btn.addEventListener('click', btnFunc) );

And finally, this is target.php, which just sends back some JSON:

<?php

header('Content-type: application/json');
echo json_encode(['foo' => 1, 'bar' => 2]);

What I would expect to be logged to the console is two instances of my JSON data. Instead, I’m getting this:

Promise { <state>: "pending" }
Object { foo: 1, bar: 2 }

Why is the promise still pending when I’ve returned a response to it from asyncFunc? How can I get the JSON back out of the fetch request so I can work with it? I’ve been looking at tutorials, but they all seem to stop with logging it to the console inside the fetch function, and I feel like it’s the next step I’m missing.

You seem to be misunderstanding how promises and maybe specifically how .then() works.

The .then function returns a new promise that is resolved with whatever the value returned by the callback function is, so your output variable is assigned that new promise. You then log your output variable and you see in the console that it is a promise (your first log message).

Later, the original json promise resolves and your .then callback is run with the result, which you then log as the response variable and see is the json you expect (your second log message).

If you want your output variable to contain the JSON result, you need to await your asyncFunction rather than call .then().

async function btnFunc(){
    const output = await asyncFunc();
    console.log(output);
}
1 Like

You’re right, I was not understanding .then() correctly, thank you for the clarification!

So basically, in my original code, await fetch('/target.php'); created a promise and resolved it by getting the JSON and returning it.

But because I’d tacked a .then onto that function, that created a new promise that wasn’t getting resolved at all, and that’s what was sitting in pending. Is that the idea?

The response.json() function returns a promise that resolves with the decoded JSON, rather than returning the decoded JSON data immediately. That’s where your initial promise that is returned by the asyncFunc() call comes from. Calling .then() on that promise then creates and returns a new promise that resolves after the callback is run.

The idea behind .then() is to create a chain promises that resolve in order. Each step in the chain is given the data returned by the previous step. There’s one part of the fetch API you didn’t account for in your code, and that’s if the request fails due to a server issue. fetch considers the request to be successful even if the server responds with an error code like 500. You can check the response.ok property to ensure the request was actually a success. If you add this check, and use the traditional .then callback chain the code would look like this:

function btnFunc(){
    fetch('/target.php')
        .then(function(response){
            if (!response.ok){
                throw new Error('Request failed.');
            }

            return response.json();
        })
        .then(function(decodedJson){
            //Do something with the decoded json data
            console.log(decodedJson);
        })
        .catch(function(error){
            console.log('There was an error.');
        });
}

The initial fetch call returns a promise. Calling .then creates a new promise that will resolve after the initial promise is resolved, using the data that is returned from the callback function. If you return a promise in the .then callback, it is resolved before continuing the chain. The .catch at the end is used to handle any errors that occur along the chain.

This chain always returns promises, there’s no way to return a final result and assign it to a variable. All your code that needs to do something with the results needs to be in a .then callback somewhere. In some cases, this can get to be either very long or very deep with callback functions and get hard to follow. This is where the async/await keywords come in. They allow a promise to have a final result you can assign to something, simplifying the code. The above would simplify to:

async function btnFunc(){
    try {
        const response = await fetch('/target.php');
        if (!response.ok){
            throw new Error('Request failed.');
        }

        const decodedJson = await response.json();

        //Do something with the decoded json data
        console.log(decodedJson);
    } catch (e){
        console.log('There was an error.');
    }
}

You just have to keep in mind that any async function always returns a promise that resolves with the real return value. For example:

async function dummy(){
    return 42;
}

console.log(dummy()); //Logs Promise { <state>: "fulfilled", <value>: 42 }

If you want the real return value, you must either await it, or use a traditional .then callback chain.

2 Likes

Lot to digest here. Thank you for the explanation–that helped a lot!

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.