Problem with promises and asynchronous operations - javascript

I have to make an HTTP POST request to the server, wait for the server response and then make another request (which will send some data of the server response back to the server). I thought it was an easy task and I did something like this:
const promise = new Promise((resolve, reject) => {
resolve(this.updatePU(name, 'text', selectedOption.code));
})
promise.then(() => {
console.log('Calling checkExpress function');
let express = '';
const puOrderNo = this.props.puOrderNo;
fetch('/CheckExpress', {
crossDomain: true,
method: 'POST',
headers: {
'Accept': 'text/xml',
'Content-Type': 'application/json',
},
body: JSON.stringify({"puOrderNo": puOrderNo })
})
.then(response => response.text())
.then(str => {
express = convert.xml2json(str);
}).then(() => {
const data = JSON.parse(express);
const checkExpress = data['elements'][0].elements[0].elements[0].elements[0].elements[0].text;
console.log('checkExpress:', checkExpress);
if(checkExpress === 'true'){
this.props.updatePackageTypeField(true)
} else {
this.props.updatePackageTypeField(false);
}
})
.catch(err => console.log(err));
})
The updatePU function is also an asynchronous function:
updatePU = (name, type, value) => {
const PUOrderNo = this.props.puOrderNo;
const updatedValue = type === 'text' ? (`'${name}': '${value}'`) : (`'${name}': ${value}`);
fetch('/ModifyPUOrder', {
crossDomain: true,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
updatedValue: updatedValue,
puOrderNo: PUOrderNo
}),
})
.then(response => {
if(response.ok){
return response.json();
} else {console.log(response)}
throw new Error('Request failed!');
}, networkError => {
console.log(networkError.message);
})
.then(data => {
if ("error" in data) {
alert(data.error.message);
this.refresh();
}
this.props.updatePUOrderForm(data);
});
}
The result is that promise is ignored (I think) and the second request is made before the first one! I get that the problem is that the function that is resolved in promise is an asynchronous function as well but I am not sure what to do.
Any help would be much appreciated!

The primary problem is that updatePU doesn't return a promise. You should return the result of the promise chain by adding return in front of fetch:
return fetch('/ModifyPUOrder', {
Then in your code at the top, don't create a new promise, use the one from updatePU:
this.updatePU(name, 'text', selectedOption.code)
.then(() => {
console.log('Calling checkExpress function');
// ...
There's a second (largely unrelated) problem: You're converting network error (rejection) to a fulfillment:
.then(response => {
if(response.ok){
return response.json();
} else {console.log(response)}
throw new Error('Request failed!');
}, networkError => { // ***
console.log(networkError.message); // *** Converts rejection to fulfillment with `undefined`
}) // ***
Remember that every handler in the chain transforms what passes through it. A rejection handler that doesn't throw or return a promise that rejects converts rejection to fulfillment.
Rather than adding console.log to dump errors everywhere, just propagate the chain and, at the top level where you can't propagate the chain any further, just add a final .catch that reports/handles the error (which you do have in your first code block).
See *** comments for notes on that and a couple of other things:
updatePU = (name, type, value) => {
const PUOrderNo = this.props.puOrderNo;
const updatedValue = type === 'text' ? (`'${name}': '${value}'`) : (`'${name}': ${value}`);
return fetch('/ModifyPUOrder', {
crossDomain: true,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
updatedValue: updatedValue,
puOrderNo: PUOrderNo
}),
})
.then(response => {
if(response.ok){
return response.json();
} // *** No console.log here
throw new Error('Request failed!'); // *** I wouldn't hide what happened, perhaps: `throw new Error("HTTP error " + response.status);`
}/* ***No rejection handler here */)
.then(data => {
if ("error" in data) {
alert(data.error.message);
this.refresh();
// *** You presumably want to return here or use `else` so you don't update the form below...?
}
this.props.updatePUOrderForm(data);
});
}

Related

Managing fetch errors with catch() doesn't work [duplicate]

Here's what I have going:
import 'whatwg-fetch';
function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
throw(error);
})
});
};
}
function status(res) {
if (!res.ok) {
return Promise.reject()
}
return res;
}
EDIT: The promise doesn't get rejected, that's what I'm trying to figure out.
I'm using this fetch polyfill in Redux with redux-promise-middleware.
Fetch promises only reject with a TypeError when a network error occurs. Since 4xx and 5xx responses aren't network errors, there's nothing to catch. You'll need to throw an error yourself to use Promise#catch.
A fetch Response conveniently supplies an ok , which tells you whether the request succeeded. Something like this should do the trick:
fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong');
})
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
The following login with username and password example shows how to:
Check response.ok
reject if not OK, instead of throw an error
Further process any error hints from server, e.g. validation issues
login() {
const url = "https://example.com/api/users/login";
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
email: this.username,
password: this.password,
}),
})
.then((response) => {
// 1. check response.ok
if (response.ok) {
return response.json();
}
return Promise.reject(response); // 2. reject instead of throw
})
.then((json) => {
// all good, token is ready
this.store.commit("token", json.access_token);
})
.catch((response) => {
console.log(response.status, response.statusText);
// 3. get error messages, if any
response.json().then((json: any) => {
console.log(json);
})
});
},
Thanks for the help everyone, rejecting the promise in .catch() solved my issue:
export function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
return Promise.reject()
})
});
};
}
function status(res) {
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
}
For me,
fny answers really got it all. since fetch is not throwing error, we need to throw/handle the error ourselves.
Posting my solution with async/await. I think it's more strait forward and readable
Solution 1: Not throwing an error, handle the error ourselves
async _fetch(request) {
const fetchResult = await fetch(request); //Making the req
const result = await fetchResult.json(); // parsing the response
if (fetchResult.ok) {
return result; // return success object
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
const error = new Error();
error.info = responseError;
return (error);
}
Here if we getting an error, we are building an error object, plain JS object and returning it, the con is that we need to handle it outside.
How to use:
const userSaved = await apiCall(data); // calling fetch
if (userSaved instanceof Error) {
debug.log('Failed saving user', userSaved); // handle error
return;
}
debug.log('Success saving user', userSaved); // handle success
Solution 2: Throwing an error, using try/catch
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
let error = new Error();
error = { ...error, ...responseError };
throw (error);
}
Here we are throwing and error that we created, since Error ctor approve only string, Im creating the plain Error js object, and the use will be:
try {
const userSaved = await apiCall(data); // calling fetch
debug.log('Success saving user', userSaved); // handle success
} catch (e) {
debug.log('Failed saving user', userSaved); // handle error
}
Solution 3: Using customer error
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
throw new ClassError(result.message, result.data, result.code);
}
And:
class ClassError extends Error {
constructor(message = 'Something went wrong', data = '', code = '') {
super();
this.message = message;
this.data = data;
this.code = code;
}
}
Hope it helped.
2021 TypeScript Answer
What I do is write a fetch wrapper that takes a generic and if the response is ok it will auto .json() and type assert the result, otherwise the wrapper throws the response
export const fetcher = async <T>(input: RequestInfo, init?: RequestInit) => {
const response = await fetch(input, init);
if (!response.ok) {
throw response;
}
return response.json() as Promise<T>;
};
and then I'll catch errors and check if they are an instanceof Response. That way TypeScript knows that error has Response properties such as status statusText body headers etc. and I can apply a custom message for each 4xx 5xx status code.
try {
return await fetcher<LoginResponse>("http://localhost:8080/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "user#example.com", password: "passw0rd" }),
});
} catch (error) {
if (error instanceof Response) {
switch (error.status) {
case 401:
throw new Error("Invalid login credentials");
/* ... */
default:
throw new Error(`Unknown server error occured: ${error.statusText}`);
}
}
throw new Error(`Something went wrong: ${error.message || error}`);
}
and if something like a network error occurs it can be caught outside of the instanceof Response check with a more generic message i.e.
throw new Error(`Something went wrong: ${error.message || error}`);
The answer by #fny (the accepted answer) didn't work for me. The throw new Error() wasn't getting picked up by the .catch. My solution was to wrap the fetch with a function that builds a new promise:
function my_fetch(url, args) {
return new Promise((resolve, reject) => {
fetch(url, args)
.then((response) => {
response.text().then((body) => {
if (response.ok) {
resolve(body)
} else {
reject(body)
}
})
})
.catch((error) => { reject(error) })
})
}
Now every error and non-ok return will be picked up by the .catch method:
my_fetch(url, args)
.then((response) => {
// Do something with the response
})
.catch((error) => {
// Do something with the error
})
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
fetch("https://example.com/api/users")
.then(handleErrors)
.then(response => console.log("ok") )
.catch(error => console.log(error) );
I wasn't satisfied with any of the suggested solutions, so I played a bit with Fetch API to find a way to handle both success responses and error responses.
Plan was to get {status: XXX, message: 'a message'} format as a result in both cases.
Note: Success response can contain an empty body. In that case we fallback and use Response.status and Response.statusText to populate resulting response object.
fetch(url)
.then(handleResponse)
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
export const handleResponse = (res) => {
if (!res.ok) {
return res
.text()
.then(result => JSON.parse(result))
.then(result => Promise.reject({ status: result.status, message: result.message }));
}
return res
.json()
.then(result => Promise.resolve(result))
.catch(() => Promise.resolve({ status: res.status, message: res.statusText }));
};
I just checked the status of the response object:
$promise.then( function successCallback(response) {
console.log(response);
if (response.status === 200) { ... }
});
Hope this helps for me throw Error is not working
function handleErrors(response) {
if (!response.ok) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({
status: response.status,
statusText: response.statusText,
});
}, 0);
});
}
return response.json();
}
function clickHandler(event) {
const textInput = input.value;
let output;
fetch(`${URL}${encodeURI(textInput)}`)
.then(handleErrors)
.then((json) => {
output = json.contents.translated;
console.log(output);
outputDiv.innerHTML = "<p>" + output + "</p>";
})
.catch((error) => alert(error.statusText));
}
Another (shorter) version that resonates with most answers:
fetch(url)
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(json => doStuff(json)) //all good
//next line is optional
.catch(response => handleError(response)) //handle error

React Native get return value of async function

I am trying to get pub locations data from MYSQL server and my fetch function works well. But after that, this try-catch block does not return anything. I also tried without try-catch block but it does not change anything
getPubsFromDatabase = async () => {
let response = await fetch(fetchDataUrl, {
method: 'POST',
headers:
{
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
try{
let json = await response.json();
console.log(json)
return json;
}
catch (error) {
console.log(error);
}
}
And here, I am trying to get the return value of the function. But in this version, I cannot even see any console.log output. What I mean by the version is, if I put 2nd line out of the async block without "await" keyword, I can see the console.log but I it gives "undefined" then.
(async () => {
const locationsData = await getPubsFromDatabase();
console.log(locationsData)
})()
You can use then and catch in the function fetch.
const getPubsFromDatabase = () => {
return fetch(fetchDataUrl, {
method: 'POST',
headers:
{
'Accept': 'application/json',
'Content-Type': 'application/json',
},
}).then(async (response) => {
const json = await response.json().then((data)=>{
return data;
}).catch((err)=>{
console.log('error from json method: ',err);
return { error: {
message: 'error from json method',
object: err
}};
});
console.log(json);
return json;
}).catch((error) => {
console.log('error from request: ', error);
return {
error: {
message: 'error from request', object: error
}
};
});
}
And when you use the method getPubsFromDatabase would be of the next way:
(async () => {
const locationsData = await getPubsFromDatabase();
console.log(locationsData);
})()
You can use Promise to return either result or error, In the example given below i have used axios library but you can also try same with fetch api
For Example
export const getData = () => {
return new Promise((resolve, reject) => {
const url = ApiConstant.BASE_URL + ApiConstant.ENDPOINT;
const API_HEADER = {
"Authorization": token,
};
axios
.get(url, {
headers: API_HEADER,
})
.then((res) => {
resolve(res.data);
})
.catch((error) => {
// handle error
reject(error);
console.log(error);
});
})
}
You can call the above function in component or screen like this:
getData().then(
(data) => {
}
).catch(
error => {
console.log(error)
}
)
I solved the problem by instead of returning the value from the function, I set the value inside the function. This seems to work now.
const [locationsData, setLocationsData] = useState();
getPubsFromDatabase = async () => {
let response = await fetch(fetchDataUrl, {
method: 'POST',
headers:
{
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
try{
let json = await response.json();
setLocationsData(json); // Here, I changed the value of the locationsData instead of returning
}
catch (error) {
console.log(error);
}
}
And to call the function:
useEffect(()=> {
getPubsFromDatabase();
},[])

How can I console log the promise result instead of the promise

I have routes set up for a local api. So I am trying to retrieve the api data console log just the data. But whenever I run my code it console logs the entire promise. Any ideas on how to help?
This is my code:
const onPageLoad = async () => {
const response = await fetch(`/api/project/projects`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (response.ok) {
console.log(response.json())
} else {
console.error(err)
}
}
onPageLoad();
This is what shows in the console log:
You can await the promise from response.json() to get its value:
const onPageLoad = async () => {
const response = await fetch(`/api/project/projects`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (response.ok) {
let value = await response.json();
console.log(value);
// potentially do something else with value
return value;
} else {
console.log('fetch() promise succeeded, but not with response.ok', response.status);
throw new Error(`Got status ${response.status}`);
}
}
onPageLoad().then(() => {
console.log('all done');
}).catch(err => {
console.log(err);
});

Axios catch not working when used with a customer interceptor

I am using interceptor that refreshes the access token upon a status code of 401 but my catch has stopped working upon any other error codes such as 400
request.js
import axios from 'axios'
const axiosApiInstance = axios.create()
axiosApiInstance.interceptors.response.use(
(response) => {
return response
},
(error) => {
return new Promise((resolve) => {
const originalRequest = error.config
const refreshToken = localStorage.getItem("refresh")
if (error.response && error.response.status === 401 && error.config && !error.config.__isRetryRequest && refreshToken) {
originalRequest._retry = true
const response = fetch('http://127.0.0.1:8000/api/token/refresh/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh: refreshToken,
}),
})
.then((res) => res.json())
.then((res) => {
localStorage.setItem("access", res.access)
originalRequest.headers['Authorization'] = 'Bearer ' + res.access
return axios(originalRequest)
})
resolve(response)
}
return Promise.reject(error)
})
},
)
export default axiosApiInstance
You need to pass a second argument to your callback to new Promise called reject and then reject the promise using that function instead of Promise.reject.
I can't verify without testing, and that's a bit difficult to set up right now, but it looks very much like you're not properly rejecting the promise that you're returning from the error callback. If you can confirm that the promise is never resolved or rejected in the case of a 400, that would help confirm my suspicion. Try making this change, though:
The arguments to your new Promise callback:
return new Promise((resolve) => {
becomes
return new Promise((resolve, reject) => {
and later to reject, instead of
return Promise.reject(error)
just do this
reject(error) // return does nothing here, can be omitted
Edit:
Although, a very good point by someone that this is a promise anti-pattern. You're creating a new Promise for no reason.
import axios from 'axios'
const axiosApiInstance = axios.create()
axiosApiInstance.interceptors.response.use(
(response) => {
return response
},
(error) => {
const originalRequest = error.config
const refreshToken = localStorage.getItem("refresh")
if (error.response && error.response.status === 401 && error.config && !error.config.__isRetryRequest && refreshToken) {
originalRequest._retry = true
return fetch('http://127.0.0.1:8000/api/token/refresh/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh: refreshToken,
}),
})
.then((res) => res.json())
.then((res) => {
localStorage.setItem("access", res.access)
originalRequest.headers['Authorization'] = 'Bearer ' + res.access
return axios(originalRequest)
})
}
return Promise.reject(error)
},
)
export default axiosApiInstance
Should be something like this.

Iterating through multiple pages of an API response in a JS promise function

I have the following promise function which uses fetch to get data from an API:
const getContacts = token =>
new Promise((resolve, reject) => {
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then((data) => {
resolve(data);
})
.catch(err => reject(err));
});
This function is then called in a different file:
getContacts(token)
.then((data) => {
const contacts = data.data;
console.log(contacts);
})
.catch(err => console.error(err));
When there is a larger amount of data returned from the API, it is paginated. The response includes a link that needs to be fetched in order to get the next page. I want my code to first iterate through all pages and collect all data, then resolve the promise. When execution reaches the const contacts = data.data line, it should have data from every page (currently it returns only the first page).
What would be the best way to achieve this?
EDIT:
I tried recursion inside the getContacts function. This way I can iterate through all pages and get all data in one object, but I don't know what's the right way to resolve this back to the code, which initially called the function. The code below doesn't resolve correctly.
const getContacts = (token, allData, startFrom) =>
new Promise((resolve, reject) => {
if (startFrom) {
url = `${url}?${startFrom}`; // the api returns a set of results starting at startFrom (this is an id)
}
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then((data) => {
let nextPageExists = false;
Object.assign(allData, data.data);
data.links.forEach((link) => {
if (link.rel === 'next') {
nextPageExists = true;
getContacts(token, allData, link.uri);
}
});
if (!nextPageExists) {
resolve({ data: allData });
}
})
.catch(err => reject(err));
});
First of all, do not use the new Promise constructor when fetch already returns a promise.
Then, just use a recursive approach and chain your promises with then:
function getContacts(token, allData, startFrom) {
return fetch(startFrom ? url + '?' + startFrom : url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json()).then(data => {
Object.assign(allData, data.data);
const nextPage = data.links.find(link => link.rel === 'next');
if (!nextPage)
return allData;
else
return getContacts(token, allData, nextPage.uri);
});
}
Here's a generic function using async/await syntax.
It returns itself until currentPage equals totalPages. You can retrieve these keys from your API response.
async function getData(perPage, page, options, allData = []) {
// fetch data
let base = 'https://api.example.com';
let url = `${base}?perPage=${perPage}&page=${page}`;
let response = await fetch(url, options);
let data = await response.json();
// push this data object (or data.data... whatever) into allData array
allData.push(data);
// get 'totalPages' and 'currentPage' (or whatever your API names these)
let totalPages = data.pagination.total_pages;
let currentPage = data.pagination.current_page;
if (currentPage == totalPages) {
// you're done
return allData;
} else {
// get the next page and repeat
page++;
return getData(perPage, page, options, allData);
}
}
Calling it:
const options = {
method: 'GET',
headers: {
Accept: 'application/json',
appId: 'APP_ID',
apiKey: 'APP_KEY',
'Content-Type': 'application/json'
}
};
let perPage = 100;
let page = 1;
getData(perPage, page, options).then((data) => {
console.log(data)
}).catch((error) => {
console.log(error)
})

Categories