How to handle API errors efficiently? - javascript

When everything goes well it returns an object with status: 'OK'
If something goes wrong it returns status: 'FAILED',
Due to this approach I need to handle errors in both the try and catch blocks, e.g.
try {
const {data} = await axios.post(....);
if(data.status === 'OK') {
// do something and return
}
//handle error
} catch(e) {
// handle Error Again
}
As you can see I need to handle the errors two times. I am using redux-saga so I make another file for APIs and handle it in saga like this:
yield = ...my api
// then
if (data.status === 'OK') {
// do something and return
}
and so on... My end goal is to keep my sagas clean. Is there any way in which I can call the API then check the status in the API file? It should go to the catch block of the saga so I would have to handle the errors in my catch block only?
My end goal would be like this
function* doSomethng() {
try {
yield callapi...
//do something
} catch(e) {
// handle errors and even if the api status is 200
// but it returns an object with status 'FAILED' it should come here
}
}

You can create a wrapper function, that will add a check to the promise chain and throw on bad status.
const checkStatus = async (promise) => {
const result = await promise;
if (!result || result.status === 'FAILED') throw new Error('Request failed');
return result;
}
function* doSomething() {
try {
yield checkStatus(callapi(...));
//do something
} catch(e) {
// ...
}
}
You can also "hid"e the checkStatus call in the callapi function itself.

Related

Update state inside catch in javascript

I'm currently working on a script that sends requests to a 3rd party website, but after sometime the cookies are invalid & it throws a 403
So I've got my script to send the request, if it throws the 403 then we send VALID cookies and then RESEND the request.
However it appears that the state.device_info remains undefined after updating it inside the catch function?
(async () => {
let state = {
'access_denied': false,
'cookies': {}
};
try {
await getCookies(state);
state.device_info = await getInfo(state).catch(async (error) => {
let localError = handleError(error, state);
if(localError === 'access_denied') {
state.access_denied = true;
//now lets unlock the request & send it again!
let post = await sendValidCookies(state);
if(post.data.success === true) {
//update state.device_info with WORKING request!
state.device_info = await getInfo(state).catch(async (error) => {
console.log('Damn we got another error!');
console.log(error);
})
if(state.device_info.status === 200) {
console.log('We got our info info using the unlocked request!');
state.access_denied = false;
}
}
}
});
if(state.device_info === undefined || state.access_denied === true) {
console.log('We have an undefined value!');
console.log(state.device_info); //undefined
console.log(state.access_denied); //false
return false;
}
} catch(error) {
console.log('major error!');
}
})();
Instead of defining it inside catch, just return the value. This returned value will be assigned to state.device_info
state.device_info = await getInfo(state).catch(async (error) => {
let localError = handleError(error, state);
if(localError === 'access_denied') {
state.access_denied = true;
//now lets unlock the request & send it again!
let post = await sendValidCookies(state);
if(post.data.success === true) {
//Get result from WORKING request
const result = await getInfo(state).catch(async (error) => {
console.log('Damn we got another error!');
console.log(error);
});
if(state.device_info.status === 200) {
console.log('We got our info info using the unlocked request!');
state.access_denied = false;
}
return result;
}
}
});
Since you're using the await keyword in front of your method call getInfo(state) and you're calling .catch(...) right after, I assume getInfo(state) returns a Promise.
There are two ways you can handle Promise in JavaScript:
By explicitly call .then(result => {...}) while catching errors with .catch(error => {...}).
By putting the await keyword in front of your method call. By using this syntax, and if you want to catch errors (which you should do), you need to use the try { } catch (error) { } syntax.
I've never mix those two methods like you did and I think this might be the origin of the issue you're experiencing.
For your case, it should look like this:
try {
state.device_info = await getInfo(state);
} catch (error) {
// Your logic here...
}

Axios instance promise.all error handling

I have the following code in my own async function that uses another imported function from module which is a custom wrap of axios inside try/catch block:
async function getCharacter (realmSlug, characterName) {
try {
const [{id, name, gender, faction, race, character_class, active_spec, realm, guild, level, last_login_timestamp, average_item_level, equipped_item_level}, {pets, unlocked_battle_pet_slots},{mounts}] = await Promise.all([
getCharacterSummary(realmSlug, characterName), -- custom axios instance
getCharacterPetsCollection(realmSlug, characterName),
getCharacterMountsCollection(realmSlug, characterName)
])
....
return result;
} catch (error) {
console.log(error.code);
if (error.response.status === 404 || error.response.status === 403) {
console.error(`${getCharacter.name},${characterName}#${realmSlug}`);
}
return { name: characterName, realm: realmSlug }
}
}
The problem is that if I use promise.all according to Stackoverflow 1,2 I can not handle errors. So the problem is when I call function to execute, my errors doesn't handle in (catch) block. At all. Even if I don't need print them, anyway I receive messages in console about 404 errors, but console.log(error.code) still gives me nothing. For example:
So is there any way to handle this annoying error messages in console somehow?
For example using .catch somewhere? Or using for await ... of or rxJS instead if it's possible?
Exporting function and using .catch
Even if I export this function getCharacter in another .js file and use the following code:
const getCharacter = require('./getCharacter');
let bulkCharacters = [{realmSlug, characterName},{realmSlug, characterName},... ,n] //array of characters for getCharacter request
const promises = bulkCharacters.map(async ({realmSlug, characterName}) => {
try {
return await getCharacter(realmSlug, characterName);
} catch (e) {
console.log(e)
}
});
let test = await Promise.all(promises)
.catch(function(arrayOfPromises, err) {
// log that I have an error, return the entire array;
console.log('A promise failed to resolve', err);
return arrayOfPromises;
})
.then(function(arrayOfPromises) {
console.log(arrayOfPromises)
})
;
console.log('stop')
I still receive errors in console, without triggering catch block inside getCharacter function or this file in which this function was imported and catch block is outside the function.

Is it a good 'idea' to prevent nested promise with async / await?

I have some difficulties with a nested promise (below).
For now I'm using an async function in the catch to trigger authentication Errors.
But is it really a good way and isn't there a better way ?
The function have to send a POST request. If an authentication Error is thrown then login(), else throw the error.
If login() is fulfilled : retry the POST (and then return the results), else throw the error;
function getSomeData() {
return post('mySpecialMethod');
}
function login() {
const params = { /* some params */ }
return post('myLoginMethod', params).then(result => {
/* some processing */
return { success: true };
});
}
const loginError = [1, 101];
function post(method, params, isRetry) {
const options = /* hidden for brevity */;
return request(options)
// login and retry if authentication Error || throw the error
.catch(async ex => {
const err = ex.error.error || ex.error
if (loginError.includes(err.code) && !isRetry) { // prevent infinite loop if it's impossible to login -> don't retry if already retried
await login();
return post(method, params, true)
} else {
throw err;
}
})
// return result if no error
.then(data => {
// data from 'return request(options)' or 'return post(method, params, true)'
return data.result
});
}
Use
getSomeData.then(data => { /* do something with data */});
I'd suggest that for complex logic at least you use the async/await syntax.
Of course .then() etc is perfectly valid, however you will find the nesting of callbacks awkward to deal with.
My rule (like a lot of things in programming) is use context to make your decision. .then() works nicely when you're dealing with a limited number of promises. This starts to get awkward when you're dealing with more complex logic.
Using async / await for more involved logic allows you to structure your code more like synchronous code, so it's more intuitive and readable.
An example of two approaches is shown below (with the same essential goal). The async / await version is the more readable I believe.
Async / await also makes looping over asynchronous tasks easy, you can use a for loop or a for ... of loop with await and the tasks will be performed in sequence.
function asyncOperation(result) {
return new Promise(resolve => setTimeout(resolve, 1000, result));
}
async function testAsyncOperationsAwait() {
const result1 = await asyncOperation("Some result 1");
console.log("testAsyncOperationsAwait: Result1:", result1);
const result2 = await asyncOperation("Some result 2");
console.log("testAsyncOperationsAwait: Result2:", result2);
const result3 = await asyncOperation("Some result 3");
console.log("testAsyncOperationsAwait: Result3:", result3);
}
function testAsyncOperationsThen() {
return asyncOperation("testAsyncOperationsThen: Some result 1").then(result1 => {
console.log("testAsyncOperationsThen: Result1:", result1);
return asyncOperation("testAsyncOperationsThen: Some result 2").then(result2 => {
console.log("testAsyncOperationsThen: Result2:", result2);
return asyncOperation("testAsyncOperationsThen: Some result 3").then(result3 => {
console.log("testAsyncOperationsThen: Result3:", result3);
})
})
})
}
async function test() {
await testAsyncOperationsThen();
await testAsyncOperationsAwait();
}
test();
... But is it really a good way and isn't there a better way ?
No it's not a good idea because it hurts code readability.
You're mixing 2 interchangeable concepts, Promise.then/catch and async/await. Mixing them together creates readability overhead in the form of mental context switching.
Anyone reading your code, including you, would need to continuously switch between thinking in terms of asynchronous flows(.then/.catch) vs synchronous flows (async/await).
Use one or the other, preferably the latter since it's more readable, mostly because of it's synchronous flow.
Although I don't agree with how you're handling logins, here's how I would rewrite your code to use async/await and try...catch for exception handling:
function getSomeData() {
return post('mySpecialMethod')
}
async function login() {
const params = { } // some params
await post('myLoginMethod', params)
return { success: true }
}
const loginError = [1, 101]
async function post(method, params, isRetry) {
const options = {} // some options
try {
const data = await request(options)
return data.result
} catch (ex) {
const err = ex.error.error || ex.error
if (err) throw err
if (loginError.includes(err.code) && !isRetry) {
await login()
return post(method, params, true)
}
throw err
}
}
I obviously cannot/didn't test the above.
Also worth exploring the libraries which provides retry functionalities.
something like https://www.npmjs.com/package/async-retry
Generally this is not a big problem. You can chain/encapsulate async calls like this.
When it comes to logic it depends on your needs. I think the login state of a user should be checked before calling any API methods that require authentication.

How to re-throw the catched error in axios catch() block

In axios, why throw new Error() is not allowed inside catch()?
I am in such requirement, where if an error is returned by the server, the catch block should throw an error, which later on will be handled by redux-saga and appropriate action would be dispatched.
The API call:
export const userSignupApi = userDetails => {
const endpoint = `${URL_ROOT}${URN_SIGNUP}`;
axios
.post(endpoint, userDetails)
.then(response => {
console.log("Response: ", response);
//do something with the response
})
.catch(error => {
throw new Error(error.response.data.message);
});
};
I am getting Unhandled Rejection (Error) because of the above catch block.
Below is my saga which handles the operation:
import { call, takeLatest, put } from "redux-saga/effects";
function* handleUserSignup(action) {
try {
yield call(userSignupApi, action.userDetails);
yield put(userSignupSuccess()); //dispatching success action
} catch (error) {
yield put(userSignupFail(error)); //dispatching error action
}
}
function* watchUserSignup() {
yield takeLatest(NEW_USER.SIGNUP, handleUserSignup);
}
Edit: Why I want the above code structure? Because this makes easy to unit test the API code and the saga code.
The Promise created in userSignupAPI isn't being used anywhere (it's not even being returned), so when an error is thrown inside catch, the Promise chain resolves to an (uncaught) rejected Promise, resulting in the error.
In the caller of userSignupAPI, you should await the call, so that the try/catch inside handleUserSignup will see the thrown error:
export const userSignupAPI = userDetails => {
const endpoint = `${URL_ROOT}${URN_SIGNUP}`;
return axios
.post(endpoint, userDetails)
.then(response => {
console.log("Response: ", response);
})
.catch(error => {
throw new Error(error.response.data.message);
});
};
async function* handleUserSignup(action) {
try {
yield await call(userSignupApi, action.userDetails);
yield put(userSignupSuccess()); //dispatching success action
} catch (error) {
yield put(userSignupFail(error)); //dispatching error action
}
}
function* watchUserSignup() {
yield takeLatest(NEW_USER.SIGNUP, handleUserSignup);
}
(make sure that call returns the Promise returned by userSignupApi)
I got this working. I was doing it completely wrong. As suggested by #certianPerformance and after reading some questions and github issues, I got the right way to handle this. Instead of returning the api response, I should have returned the promise.
Here is the solution:
export const userSignupApi = userDetails => {
const endpoint = `${URL_ROOT}${URN_SIGNUP}`;
return axios.post(endpoint, userDetails);
};
Saga:
import { call, takeLatest, put } from "redux-saga/effects";
function* handleUserSignup(action) {
try {
const response = yield call(userSignupApi, action.userDetails);
response.then(response => {
const location = response.headers.location;
put(userSignupSuccess(location));
put(newUserWelcomeNote("Welcome user"));
});
} catch (error) {
const errorMessage = error.response.data.message;
yield put(userSignupFail(errorMessage));
}
}
function* watchUserSignup() {
yield takeLatest(NEW_USER.SIGNUP, handleUserSignup);
}
You are using try catch because you don't want to throw error in browser console you want to handle in catch. Throwing error in catch will remove its purpose.If you want to throw error remove try catch(which is not a recommended way)
UPDATE
For axios catch method catches any error thrown by api url. If you don't want to catch error and show it in browser, you can remove catch block or you can call the action which handles error. For more information about promise catch you can refer here

Async/Await error handling

I'm trying to handle a custom error that my async method throws, but the try catch block doesn't work appropriately.
I think the way I'm doing it should work but the error is not caught and the program terminates by displaying it in the terminal.
Here is where it throws the error:
async setupTap(tap) {
const model = this.connection.model('Tap', TapSchema);
await model.findOneAndUpdate({ id: tap.id }, tap, (err, result) => {
let error = null;
if (!result) {
throw new Error('Tap doesn\'t exists', 404);
}
return result;
});
}
Then, the error handling code:
async setupTapHandler(request, h) {
const tapData = {
id: request.params.id,
clientId: request.payload.clientId,
beerId: request.payload.beerId,
kegId: request.payload.kegId,
};
try {
await this.kegeratorApi.setupTap(tapData);
} catch (e) {
if (e.code === 404) return h.response().code(404);
}
return h.response().code(204);
}
Can someone help me?
I also looked at other topics:
Correct Try...Catch Syntax Using Async/Await
How to properly implement error handling in async/await case
You can only use await to successfully wait on an async operation if you are awaiting a promise. Assuming you are using mongoose, I don't know mongoose really well, but it appears that model.findOneAndUpdate() does not return a promise if you pass it a callback. Instead, it executes and puts the result in the callback.
In addition, doing a throw from a callback like this just throws into the database (the code that called the callback) and won't do you any good at all. To have a throw make a rejected promise, you need to either be throwing from the top level of an async function or be throwing from inside a .then() or .catch() handler or inside a promise executor function. That's where throw makes a promise rejected.
The key here is that you want to use the promise interface to your database, not the callback interface. If you don't pass a callback, then it returns a query which you can use .exec() on to get a promise which you can then use with await.
In addition, you weren't building an error object that would have a .code property set to 404. That isn't a property that is supported by the Error object constructor, so if you want that property, you have to set it manually.
I'd suggest this:
async setupTap(tap) {
const model = this.connection.model('Tap', TapSchema);
let result = await model.findOneAndUpdate({ id: tap.id }, tap).exec();
if (!result) {
let err = new Error('Tap doesn\'t exists');
err.code = 404;
throw err;
}
return result;
}
Or, with only one async operation here, there's really not much benefit to using await. You could just do this:
setupTap(tap) {
const model = this.connection.model('Tap', TapSchema);
return model.findOneAndUpdate({ id: tap.id }, tap).exec().then(result => {
if (!result) {
let err = new Error('Tap doesn\'t exists');
err.code = 404;
throw err;
}
return result;
});
}
The function findOneAndUpdate returns a promise so there should be no need for the callback. If a callback is needed and you can't update to a newer version then maybe wrap calls in a promise (under To use a callback api as promise you can do:)
Then you want to set code on error, you can't do that with the constructor.
async setupTap(tap) {
const model = this.connection.model('Tap', TapSchema);
const result = await model.findOneAndUpdate({ id: tap.id }, tap);
if (!result) {
const e = new Error('Tap doesn\'t exists');
e.code = 404;
throw(e);
}
return result;
}
async setupTapHandler(request, h) {
const tapData = {
id: request.params.id,
clientId: request.payload.clientId,
beerId: request.payload.beerId,
kegId: request.payload.kegId,
};
try {
await this.kegeratorApi.setupTap(tapData);
} catch (e) {
if (e.code === 404) return h.response().code(404);
}
return h.response().code(204);
}

Categories