Skip to content

Capture request response code of rejected promises with Promise.allSettled

I’m using Promise.allSettled to call an array of URLs and I need to capture the response code of the request(s) of the rejected promise(s). Using the value of result.reason provided by Promise.allSettled is not accurate enough to assess the reason of rejection of the promise. I need the request response code (400, 500, 429, etc.).

I have so far the below:

var response = await Promise.allSettled(urls.map(url => fetch(url)))
    .then(results => {
        var data = [];
        results.forEach((result, num) => {
            var item = {
                'req_url': urls[num],
                'result': result.status,
                'result_details': result
            };
            data.push(item);
        });
        return data;
    });

How could I capture the response code of the request of the rejected promise and add it as a property within the returned array? The returned array should look ideally like this:

[{
    'req_url': 'https://myurl.xyz/a',
    'req_status_code': 400,
    'result': 'rejected',
    'result_details': {
        'status': 'rejected',
        'message': 'TypeError: Failed to fetch at <anonymous>:1:876'
    }
},
{
    'req_url': 'https://myurl.xyz/b',
    'req_status_code': 419,
    'result': 'rejected',
    'result_details': {
        'status': 'rejected',
        'message': 'TypeError: Failed to fetch at <anonymous>:1:890'
    }
},
{
    'req_url': 'https://myurl.xyz/c',
    'req_status_code': 429,
    'result': 'rejected',
    'result_details': {
        'status': 'rejected',
        'message': 'TypeError: Failed to fetch at <anonymous>:1:925'
    }
}]

Any ideas?

Answer

fetch doesn’t reject its promise on HTTP failure, only network failure. (An API footgun in my view, which I wrote up a few years back on my anemic old blog.) I usually address this by wrapping fetch in something that does reject on HTTP failure. You could do that as well, and make the failure status available on the rejection reason. (But you don’t have to, see further below.)

class FetchError extends Error {
    constructor(status) {
        super(`HTTP error ${status}`);
        this.status = status;
    }
}
async function goFetch(url, init) {
    const response = await fetch(url, init);
    if (!response.ok) {
        // HTTP error
        throw new FetchError(response.status);
    }
    return response;
}

Then you could pass an async function into map to handle errors locally, and use Promise.all (just because doing it all in one place is simpler than doing it in two places with Promise.allSettled):

const results = await Promise.all(urls.map(async url => {
    try {
        const response = await goFetch(url);
        // ...you might read the response body here via `text()` or `json()`, etc...
        return {
            req_url: url,
            result: "fulfilled",
            result_details: /*...you might use the response body here...*/,
        };
    } catch (error) {
        return {
            req_url: url,
            result: "rejected",
            result_status: error.status, // Will be `undefined` if not an HTTP error
            message: error.message,
        };
    }
}));

Or you can do it without a fetch wrapper:

const results = await Promise.all(urls.map(async url => {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            // Local throw; if it weren't, I'd use Error or a subclass
            throw {status: response.status, message: `HTTP error ${response.status}`};
        }
        // ...you might read the response body here via `text()` or `json()`, etc...
        return {
            req_url: url,
            result: "fulfilled",
            result_details: /*...you might use the response body here...*/,
        };
    } catch (error) {
        return {
            req_url: url,
            result: "rejected",
            result_status: error.status, // Will be `undefined` if not an HTTP error
            message: error.message,
        };
    }
}));