There are two hard problems in computer science: Cache invalidation and naming things. In this post we'll show how memoize an async function, and how to invalidate the memoization when the promise throws an error.
This helps us with being able to re-try because since the error is not cached, calling it again after an error retries automatically.
Example async function:
Here is a technique that can be used to memoize this function
The promise is held in this.promise, and the important part of this function is that when I get an error, I clear this.promise and re-throw the error. The caller of the function, on error, will receive the error message, but caching will not take place, allowing retries to take place later on.
If your function takes arguments, then you can use a hash map associating the argument with the promise. You may also consider using an LRU cache so that your hash map doesn't grow infinitely in size
Generally you need a way to stringify or otherwise make them able to be stored in a Map or Object to do this.
fetch
This demo also demonstrates some basic fetch error handling, and uses
await response.text()
to get the error message from the API. Sometimes an api
will return it's error in JSON format, so you can handle that as is, sometimes
you have to check both text and json
Note also, that response.statusText does
not exist in HTTP/2
so it's better to use response.text()
or response.json()
.
You could also keep a cache in a global variable, or as a property on a class, or other methods. I have also found it useful to have a specific function for clearing the cache, so you can get a clean slate each time a test runs in unit testing or similar
You can also make a general purpose utility to memoize any promise function
If you want to handle aborting, it is a bit trickier. Aborting in javascript is handled by AbortController. This is an object that gives you an AbortSignal that can be passed to fetch calls and the like to stop a big download from happening.
In our above example, if we passed an abort signal to the first call to fetch, and then aborted it, it would abort the fetch, which throws a DOMException called "AbortError". You can detect that it is an AbortError like this, and may choose not to display or re-throw the abort exception
Now, what if 5 functions call getDataMemoized(), all passing different abort signals. What if the first one aborts? Then all the rest will get aborted also. But what if we only want to abort the cached call if literally all of them aborted? Then we may have to synthesize an AbortController inside our function
My team created, abortable-promise-cache, tries to help with this scenario with a cleaner abstraction.
This blog post mentioned in a comment thread https://zansh.in/memoizer.html has great interactive examples and shows the "invalidate on .catch()" behavior!