A simple wrapper around fetch
If you need to account for mixed response types (in your case &quot;unknown errors&quot; or JSON-formatted errors), then you could consider checking the Content-Type of the response. See this StackOverflow answer for reference. This, of course, trusts in the server echoing the correct content type header for a response.
As you mentioned, fetch does not reject on HTTP error status, instead response.ok will be set to false. You can use this to decide if you want to reject or resolve your promise.
/* ... */.then(async response =&gt; {
    const contentType = response.headers.get(&quot;content-type&quot;);
    if (contentType &amp;&amp; contentType.indexOf(&quot;application/json&quot;) !== -1) {
        const json = await response.json();
        return response.ok
            ? Promise.resolve(json)
            : Promise.reject(json);
    } else {
        const text = await response.text();
        return response.ok
            ? Promise.resolve(text)
            : Promise.reject(new Error(&quot;Unknown Error!&quot;));
    }
});

You could also (which is what the post linked above suggests aswell) use a try-catch block, if you do not trust the response headers.
/* ... */.then(async response =&gt; {
    const text = await response.text();
    try {
        const data = JSON.parse(text);

        // If the line above has not produced an error, the response contained valid JSON.
        // We can either choose to just return the resulting data, or, as in
        // example above, resolve/reject the Promise based on the status code.

        return response.ok
            ? Promise.resolve(data);
            : Promise.reject(data);
    }
    catch(err) {
        // This means that the response probably contained text. As above, you can
        // decide if this means an error has occurred, or if the result will just
        // be handled differently.

        return response.ok
            ? Promise.resolve(text);
            : Promise.reject(new Error(&quot;Unknown Error!&quot;));
    }
});


As for your additional question on the difference between:
if (response.ok) {
    const json = await response.json();
    return json;
}
// ... and ...
if (response.ok) {
    return response.json();
}

This essentially boils down to the difference between:
return await response.json();
// ... and ...
return response.json();

The difference lies within the way that JavaScript handles async/await methods. The first line returns a generic T, while the second one returns Promise&lt;T&gt;. What this means is that the first one &quot;delays&quot; the execution of the return statement until the promise returned by response.json() is resolved, while the second one says &quot;I don't care that execution is continued, whoever calls me later will have to wait for my value to be resolved.&quot;, which is what happens when you perform .then(...) on a promise.
Note that the first method still ultimately returns a Promise&lt;T&gt; though, since async methods are essentially just fancy wrappers for promise generations.