Expected Errors
Expected errors are client errors. For example, if we have an invalid id, the server will give us a 404 not found.
For example,
1 2 |
const apiEndpoint = 'http://jsonplaceholder.typicode.com/posts'; axios.delete(apiEndpoint); |
If you try to do a HTTP delete something providing a parameter id, you’ll get a 404 error because the server api endpoint is expecting it as part of its interface.
Unexpected Errors
Unexpected errors are network errors, server errors, db errors. The response status will be less than 400, and larger than 500.
Conceptually, we’d handle errors like so.
First, we update our local UI right away…in what we call an optimistic error handling. We are optimistic that the server update will be successful so we update our local UI right away. Then, we do the server update after. This gives a nice UI flow.
Notice we have a reference to the posts. This is so that in case the server update has an error, we can revert back to our original state by reassigning it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
handleDelete = async post => { const originalPosts = this.state.posts; // filter out all the posts that IS NOT THIS post const posts = this.state.posts.filter(p => p.id !== post.id); this.setState({ // update local UI right away. posts }); try { // await axios.delete(apiEndpoint/*+post.id*/); // expected error. user id is not valid await axios.delete('s' + apiEndpoint/*+post.id*/); // unexpected error, erroneous URL goes to server that does not exist. } catch (ex) { if (ex.response && ex.response.status === 404) { alert('this post has already been deleted'); // revert back to original this.setState({ posts: originalPosts }); } else { // use a logging method to store or show the unexpected errors console.log(error); } } } |
So, when an expected error occurs (such as invalid ID), we see the alert, then we’ll see that it inverts back to the original state list. This is good and simple way to handle it.
This is the result of an unexpected error. For example, we give it a weird URL where it does not reach a server. In normal circumstances, this would either be a server down, db down, or no internet (Network down) error. In which, you’ll probably get a 500 status. For now, we simply log it.
Improving it with Axios
The problem with our previous approach is that we need global handling. But we cannot possibly do try/catch for all the different places that we need error handling. Hence, we install Axios to do this:
npm install axios@0.18.1
Axios has an interceptor where we check to see what kind of error it has. That way, all errors from using axios will have to go through this interceptor.
The first parameter is success. In our case, we don’t need to deal with it, so just give it a null. The second parameter is a reference to a callback for when we receive an error. Exactly what we need. So we give it a callback function with an error parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
axios.interceptors.response.use(null, error => { let expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log('Logging Error', error); // logging error alert('An unexpected error occured') } return Promise.reject(error); }); |
We check to see if the error has a response object. If it does, look at its status. If it’s 400 or more, and less than 500, it means it’s an expected error. This is when you didn’t supply the correct parameters or didn’t satisfy the endpoint interface. In which case, simply just return the error.
However, if it’s an unexpected error, we get to alert it. As you can see, we write this just once. All the other places where we use axios to execute HTTP requests will come here when there’s an error. Let’s try testing it at componentDidMount:
1 2 3 4 5 6 7 8 9 10 |
async componentDidMount() { const promise = axios.get('s'+apiEndpoint); console.log(promise); // Promise object const response = await promise; // always make sure outer function has async keyword console.log(response); // actualy object const { data: posts} = response; this.setState({ posts }); } |
As you can see, by giving it an erroneous URL, it goes through logging error in the interceptor.
Extracting a Reusable Http Service
However, we have superfluous code in our app module. We need to refactor.
Create a services folder, then create httpService.js file. We move all the interceptor logic in here. Also, we wrap Axios’s functionality.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import axios from 'axios'; // with interceptors, we're handling errors globally axios.interceptors.response.use(null, error => { console.log(`AXIOS INTERCEPTORS`); let expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log('Logging Error', error); // logging error alert('An unexpected error occured') } return Promise.reject(error); }); export default { get: axios.get, post: axios.post, put: axios.put, delete: axios.delete }; |
Then in App.js, we import Axios and replace all axios operations with our http module:
1 2 3 4 5 6 7 8 9 |
import http from './services/httpService'; // hide axios behind http module const { data: post } = await http.post(apiEndpoint, obj); ... const promise = http.get(apiEndpoint); ... const {data} = await http.put(apiEndpoint + '/' + post.id, post); ... ... |
Extract config Module
Create config.json file
We put constants and configuration strings there:
1 2 3 |
{ "apiEndpoint": "http://jsonplaceholder.typicode.com/posts" } |
Then in our App.js, we import it, and replace previous global constants.
1 2 3 |
import config from './config.json'; ... const { data: post } = await http.post(config.apiEndpoint, obj); |
Using Toast
Now we use Toast library to put some UI animation behind our error handling.
npm install Toastify@4.1.0
In our App.js, right below our React Fragment, we put the ToastContainer. The point is to put our handlers that have try/catch and axios actions within this ToastContainer. Any kind of errors that bubble out from the JSX will be captured by this ToastContainer.
App.js
1 2 3 4 5 6 |
render() { return ( <React.Fragment> <ToastContainer /> ... ... |
We go to our http module where we throw out errors and put the needed error messages:
httpService.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import axios from 'axios'; import { toast } from 'react-toastify'; // with interceptors, we're handling errors globally axios.interceptors.response.use(null, error => { console.log(`AXIOS INTERCEPTORS`); let expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log('Logging Error', error); // logging error toast.error("An unexpected error occureed"); } return Promise.reject(error); }); |
As long as you remember to import the toast function from ‘react-toastify’, you can use the Toast UI animation.