In the past two months I explored a lot of things Express, Fastify, MongoDB, PostgreSQL, Node.js. I built around three to four projects in this time.
All of them focused on backend because I haven't learned React yet. But the next one will be full stack.
While building these projects, I noticed one issue that kept repeatedly coming back.
Every time I called a database or an external service something that depends on a network call things would get weird.
Sometimes the response never came. Sometimes it took way longer than it should. And this was creating a performance issue in my application that I couldn't ignore.
So I investigated it properly. And I found that this is actually a common problem a lot of developers face this. And there's a standard solution for it.
The Solution is a Timeout Wrapper
The idea is simple. When you make a network call, instead of just calling it directly, you pass it through a timeout wrapper.
If the call takes more time than it should, the wrapper skips or terminates the call and moves on.
This keeps your application from getting stuck waiting on something that's never going to respond in time.
Two Functions : Two Different Approaches In Timeout
While building this, I found there are two ways to implement a timeout wrapper. Both solve the same problem but work very differently.
1. withTimeout()
export function withTimeout(promise, ms, errorMessage = "Operation timed out") {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(errorMessage)), ms)
)
return Promise.race([promise, timeout])
}
This function takes two things the promise (which is your network call) and the time limit in milliseconds.
Inside, it creates a second promise called timeout. This promise does one thing if the given time runs out, it rejects with an error message.
Then it uses Promise.race(). This function takes both promises and returns whichever one finishes first.
If your network call responds in time, Promise.race gives you that response. If the timeout fires first, you get the error.
Simple. Clean. Works.
But there's a catch I found and this is the important part.
withTimeout() doesn't actually terminate the original network call. If the timeout fires and you move on, the network call is still running in the background.
The socket is still open. Memory is still being used. CPU is still consumed. The request finishes eventually you just stopped waiting for it. The resources are still gone.
This is where the second function solves a different problem.
2. fetchWithTimeout()
export function fetchWithTimeout(url, options = {}, timeoutMs = 10_000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timer))
}
This function takes the URL, options for the network call, and the timeout.
The key difference here is AbortController. This is a browser and Node.js API specifically built for canceling asynchronous operations without creating memory leaks.
You create a controller, attach its signal to the fetch call, and start a timer. If the timer runs out before the call responds controller.abort() is called.
The fetch is terminated. Not just skipped actually terminated. All the resources that were being consumed get freed.
And the finally() at the end makes sure the timer gets cleared if the call does respond in time. No timer left hanging.
The Real Difference Between Both
This is the thing I want to make clear because it took me a bit to fully understand it.
withTimeout() skips the wait. If the call takes too long, you move on. But the call is still running. Resources are still being consumed. You just stopped caring about the response.
fetchWithTimeout() terminates the call. If it takes too long, the request is actually cancelled. Network socket freed. Memory freed. CPU freed.
For most external API calls where you're using fetch, fetchWithTimeout is the right choice because it actually cleans up after itself.
For other types of async operations database queries, file reads, anything that returns a promise withTimeout is what you'd use. Just know that the operation continues in the background even after the timeout fires.
Why This Actually Matters
When I was building my projects and network calls were just hanging my application was stuck. The user was waiting. The server was waiting. Nothing was happening.
After adding timeout wrappers, if a call hangs, the application moves on. The user gets a proper error response instead of waiting forever. And with fetchWithTimeout, the resources actually get freed instead of piling up in the background.
This is the kind of thing that feels small but makes a real difference in how your application behaves under real conditions.
One investigation. One problem. Two functions that solved it.
If I got something wrong or anything can be improved please drop it in the comments. I'm still learning and I want to get this right.
Top comments (0)