Published 2024-11-05 by Daniel Bark
Improving fetch with Effect
Effect has many value propositions.
- Type safety in the Error channel
- Controlled concurrency via Fiber runtimes
- Runtime Schema validation
- Easy Scheduling and Retry policy management
This article focuses on Retries and a small win for almost any application that uses HTTP.
Retrying network calls is quite often ignored. Probably because the implementations tend to get a bit messy. Thats why I want to push you to incorporate small pieces of Effect in your apps.
First we wrap our fetch with try…catch blocks in Effect.tryPromise({})
const goFetch = (url: string) => Effect.tryPromise({
try: async () => {
const response = await fetch(url)
if (response.status !== 200) {
throw new HttpError({ status: response.status });
}
return response
},
catch: response => {
if (hasStatusProperty(response)) {
return new HttpError({ status: response.status });
}
return new UnknownFetchError()
}
})
Then we create 2 TaggedErrors. Tagged means we have a _tag prop with a unique string in order to distinguish types that share the same shape but should not be interchangeable.
class HttpError extends Data.TaggedError("HttpError")<{
status: number
}> { }
class UnknownFetchError extends Data.TaggedError("UnknownFetchError") { }
Now if we look at the type of our Effectful fetch. An Effect that succeeds with a Response or Fails with a http status code or does something else completely unexpected.
Effect<Response, HttpError | UnknownFetchError, never>
Now we want to incorporate a retry policy if we get back any of the following HTTP codes:
- 408, Request Timeout
- 429, Too Many Requests
- 503, Service Unavailable
- 504 Gateway Timeout
And optionally:
- 500, Internal Server Error
- 502, Bad Gateway
We put these numbers in a Set called RETRY_CODES
Now we can put out fetch in an Effect pipe with a retry.
const fetchWithRetry = (url: string, maxRetries: number) => pipe(
goFetch(URL),
Effect.retry({
times: maxRetries,
while: (err) => {
return hasStatusProperty(err) && RETRY_CODES.has(err.status)
}
}),
)
Now all we have to do is execute the Effect with Effect.runPromiseExit
and inspect the results.
We wrap everything in safeFetchWithRetry
and hide the somewhat esoteric Effect types internally and return a FetchResult
type.
type FetchResult = { success: true, res: Response } | { success: false, res: null }
async function safeFetchWithRetry(url: string, maxRetries = 3): Promise<FetchResult> {
const result = await Effect.runPromiseExit(fetchWithRetry(url, maxRetries))
const success = result._tag === "Success"
if (!success) {
return { success, res: null }
}
return { success, res: result.value }
}
Effect made this code very declarative and easy to extend.
Hope you enjoyed the read.
Written by Daniel Bark
← Back to blog