If you've ever worked on a big project, from the ground up to a live project, one puzzle you'll undoubtedly solve is error handling.
Proper error handling becomes your eye when things go wrong.
Errors from the backend are beautifully sent to the frontend.
You easily decode each error, understand, and can tell what is wrong at every point in time.
I was recently working on a project, Ideliver.
An Uber App but for drugs with an added interface - consultations.
So we'll be handling customer management (buying of drugs, booking consultation with medical personnel, etc), pharmacy inventory (drugs management, etc), consultations (chatting, video calls, etc), and driver (maps, notifications, etc).
The stack is simple. React Native for the frontend. Express and Supabase for backend services. Tanstack Query and Axios for data fetching.
Obviously, this can easily get overwhelming.
So proper structure needs to be in place to have anything work and work properly.
Before you get lost in the wind, let's get back to our topic.
The Backend Services
We have a very simple architecture for this.
Supabase as BAAS handles auth in its entirety. Think authentication, session management, etc.
Express handles Authorization, Database querying, and every other backend service as needed.
For Express, we employ a simplified MVC architecture.
Models for handling all database operations with Supabase.
Views as service classes in the front-end.
…and controllers for business logic.
Forget about the names you don't understand.
We just use them to ensure structure and separation of concerns (beautiful CS concept) in our backend.
but stay with me…
There's the good part!
For this post, we will handle backend errors in the controllers (we'll take this up a notch with global error handling in a future article).
Basically, this sends the error object to the frontend.
Here's what a controller could look like for us.
export const completeProfile = async (req, res) => {
try {
const { role, userData } = req.body;
const existingProfile = await UserProfileModel.getCustomerProfile(req.user.id);
if (existingProfile) {
return res.status(400).json({
error: "ProfileError"
message: "Profile already exists",
});
}
const result = await UserProfileModel.createProfileWithEntity(
req.user.id,
role,
userData
);
res.json({
message: "Profile completed successfully",
role,
userId: req.user.id
});
} catch (error) {
res.status(500).json({
error: "ProfileError"
message: error.message
});
}
};
Hectic!
Let's reduce our focus to this…
const existingProfile = await UserProfileModel.getCustomerProfile(req.user.id);
if (existingProfile) {
return res.status(400).json({
error: "ProfileError"
message: "Profile already exists",
});
Assuming a user somehow managed to attempt to create a profile while they already have a profile, it should trigger an error.
An error with a 400 status code.
Simply put, our response would look like this…
{
error: "ProfileError"
message: "Profile already exists",
}
So how do we handle this beautifully on the frontend?
Here's where all the good stuff happens with axios and Tanstack Query.
The good part: Axios
If you've never used Axios before, it's a promise-based HTTP client that makes API requests cleaner and more manageable than the native fetch API.
If you don't understand what all that means, follow me.
npm, pnpm, or yarn install axios.
Previously, you'll need to use "fetch" to make API requests.
…and it could look like this for our setup…
const completeProfile = async (payload) => {
const res = await fetch('/profile/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || 'Failed to complete profile');
}
return res.json();
};
With Axios, this is all you need…
const completeProfile = async (payload) => {
const { data } = await axios.post('/profile/complete', payload);
return data;
};
Am I clear? 😂
So what about error handling with Axios?
Axios throws an Axios Error object (we talked about throw previously!!!) internally when the status code of a response moves beyond 2xx.
Yes, a 400 status code will trigger an error.
In our case, the error looks like this…
{
"response": {
"status": 400,
"data": {
"error": "ProfileError",
"message": "Profile already exists"
}
}
}
If we use fetch, we definitely need to do this…
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || 'Failed to complete profile');
}
…to trigger an error, when the response is beyond 2××.
Well, Axios has simplified that already.
Tanstack Query (formerly React Query)
The error object Axios throws would usually be caught by a try and catch block if you were to follow the normal, average junior dev React routine.
This is what I mean…
const useCompleteProfile = (payload) => {
try {
const response = await completeProfile(payload)
} catch(error){
setErrorState(error)
}
}
But we're not junior devs, and we know better.
So we use Tanstack Query.
What is Tanstack Query?
TanStack Query is a powerful data-fetching and state-management library designed to simplify working with asynchronous data in React applications.
With TanStack Query, you no longer manage loading states, errors, retries, caching, or background refresh logic manually.
The library handles all of this automatically, allowing you to focus on building features instead of wiring data-handling boilerplate.
Yikes!
For our use case, here's what that means.
const useCompleteProfile = (payload) => {
return useMutation({
mutationFn: completeProfile(payload),
});
}
And in the error component we choose to use, we do something like this…
{mutation.isError && (
<View className="bg-red-100 p-3 rounded-lg">
<Text className="text-red-600">
{mutation.error?.response?.data?.message}
</Text>
</View>
)}
Voila.
Beautiful isn't it?
We just beautifully handled the error.
This isn't in any way a thorough article that should guide you on everything you need to know about these libraries, architecture, or structure.
There is so much to them!
This is however a beautiful way to manage errors from the backend to the frontend of any project.
Explore! Thanks for reading.




Top comments (0)