Answering my own question. 
Edit...
With help from Amit and Felix I've settled on the code below as it's easiest for me to read.
async function format(response) {
    const json = await response.json();
    return {response, json};
}
function checkStatus(response, json) {
    if (response.status < 200 || response.status >= 300) {
        var error = new Error(json.message);
        error.response = response;
        throw error;
    }
    return {response, json};
}
return fetch('/api/something')
    .then((response) => format(response))
    .then(({response, json}) => checkStatus(response, json))
    .then(({response, json}) => {
        console.log('Success!', json.message);
    })
    .catch((error) => {
        if (error && error.response) {
            console.log('error message', error.message);
        } else {
            console.log('Unhandled error!');
        }
    });
...End Edit
Promise.all would have worked for me as described here: How do I access previous promise results in a .then() chain?. However, I find that unreadable. So ES7 async functions to the rescue!
async function formatResponse(response) {
    var json = await response.json();
    response.json = json;
    return response;
}
function checkResponseStatus(response) {
    if (response.status >= 200 && response.status < 300) {
        return response;
    } else {
        var error = new Error(response.json.message);
        error.response = response;
        throw error;
    }
}
function handleResponse(response) {
    console.log('success!', response.json);
}
function handleError(error) {
    if (error && error.response) {
        console.log('error message', error.message);
        console.log('error response code', error.response.status)
    } else {
        console.log('Unhandled error!');
    }
}
return fetch('/api/something')
   .then(formatResponse)
   .then(checkResponseStatus)
   .then(handleResponse)
   .catch(handleError);