1) A rare case when requests run in parallel and cause a problem.
For this article, I’ll create a simple example app. The user can enter a joining date to see the bonus information of employees.
I know the business idea of this app doesn’t make much sense — it’s just for demonstration purposes 😅.
The app includes a dropdown to select a year and a search button. (We won’t disable the button while loading, because the UX guy wants a “better user experience,” LOL.)
When the user clicks Search, the app will call an API to get data and display the result in the results section.
Below is the results section, which shows a loading state while the API is pending, and the result once it’s complete.
Front-end:
Regarding the following code:
import { useState } from 'react';
interface SearchResult {
bonus: number;
dateOffBonus: number;
dummyValue: number;
}
export const AbortControllerTest = () => {
const [joinedYear, setJoinedYear] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<SearchResult | null>(null);
const handleSearch = async () => {
setLoading(true);
setResult(null);
try {
const response = await fetch('http://localhost:3001/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
joinYear: joinedYear
}),
});
const data = await response.json();
setResult(data.result);
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
};
return (
<div>
<input
type="text"
placeholder="Joined Year"
value={joinedYear}
onChange={(e) => setJoinedYear(e.target.value)}
/>
<button onClick={handleSearch}>
{'Search'}
</button>
{loading ? <div><p>loading</p></div> : result && (
<div>
<p>Bonus: {result.bonus}</p>
<p>Days Off: {result.dateOffBonus}</p>
</div>
)}
</div>
);
};
Back-end:
For the backend, we’ll have business logic like this:
When the year is before 2020, we need to call a downstream service (a fake API) that takes a lot of time to respond.
import express, { Request, Response, Application } from 'express';
import cors from 'cors';
const app: Application = express();
const port = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
const fakeApi = async (): Promise<number> => {
return new Promise(resolve => {
setTimeout(() => resolve(23000), 8000);
});
};
app.post('/api/search', async (req: Request, res: Response) => {
const { joinYear } = req.body;
let result = {
bonus: 22000,
dateOffBonus: 2,
};
if (+joinYear < 2020) {
const bonus = await fakeApi(); // Imagine if an employee joined before 2020 — in that case, we call a downstream service to get the bonus, which takes a very long time.
result.bonus = bonus
result.dateOffBonus = 3
}
res.json({
result
});
});
app.listen(port, () => {
console.log('🚀 Express REST Server ready');
});
Let's run the code:
When we choose 2011 and click Search, the app enters the loading state.
Before the first request finishes, we change the dropdown to 2023 and trigger another search. Suppose the user got tired of waiting or decided to try a different year.
The new API call completes quickly (since no downstream service is involved), and the result appears right away.
However, the previous flow (the one triggered when we searched for 2011) wasn’t actually canceled. It eventually finishes and triggers a state change.
As a result, after a short while, the user ends up seeing the 2011 result on the screen, even though the dropdown shows 2023.
It’s a very rare bug because, in most cases, the previous request finishes before the user changes to the next one. However, my team and I have encountered this issue several times.
2) Solution: AbortController



Top comments (0)