Its an incorrect assumption, that a deliberate coding like await[3000] is messing the code.
A user can deliberatly mess up same code like this too:
Believe it or not, some people write code that's (almost) completely impossible to mess up its internals. For example, they never call myArray.map()
directly, instead, they pick off map
from Array.prototype
, then use map.call()
whenever they need to map over array values. That way, no one can monkeypatch prototypes and cause the library to behave in unexpected ways. The library itself may also choose to freeze their exports so that you can't monkey patch them either.
There's people out there who really do care about being untouchable. I find this practice to be a bit overkill for some situations, but it's understandable for other situations, e.g. Node follows this practice, because it would be weird if their built-in functions changed behaviors when you monkeypatched some globals.
As for your concept in general, I still don't think it'll work great, so I've prepared a number of counter examples. For one, you're relying on .then()
to be called often. What if, it's only a single async step that's taking forever. For example, how would you protect against this?
async function doLongRunningTask() {
const connection = await getConnectionFromPool();
await new Promise((resolve) => setTimeout(resolve, Infinity)) // This will never end
return new Promise((resolve, reject) => {
...
});
}
Also, note that your current await[...]
solution has no effect on functions such as the following, where the first thing they do is construct a new promise, and everything within the promise is done via callbacks.
function myAsyncFn() {
return new Promise(resolve => {
callback1(resource1 => {
const mappedResources = resource1.map(entry => {
return process(entry)
})
callback2(mappedResources, resource2 => {
resolve(entry)
})
})
})
}
In this scenario, your special error-throwing promise won't even get made until after all of this long-running callback code has finished executing. You're unable to stop it in the middle.
Here's another example, borrowing off @aclaymore's thoughts:
let promisedResultsCache = new Map()
async function getUser(userId) {
if (promisedResultsCache.has(userId)) {
return promisedResultsCache.get(userId)
}
const promise = getUser_(id)
promisedResultsCache.set(userId, promise)
return promise
}
async function getUser_() { ... }
// ... elsewhere ...
await getUser(1) // This promise ends up going in the cache also.
await[100] getUser(1)
// This will timeout after 100ms.
// This will also cause anyone else who was trying to get a user with id 1 to timeout as well.
Now you're causing other people's promises to throw timeout errors.
Another place where this could be a problem is, for example, if you're using an async function that "rate limits" itself, by making it so only a certain number of active promises can be created by that function at a time. Here's a simplified implementation that only allows one active promise at a time.
let currentlyRunningTask = Promise.resolve()
let tasksInQueue = 0
export async function fetchResource(...args) {
return new Promise((resolve, reject) => {
tasksInQueue++
currentlyRunningTask = currentlyRunningTask.then(() => {
tasksInQueue--
try {
resolve(await fetchResources_(...args))
} catch (err) {
reject(err)
}
})
})
}
export const getTasksInQueue = () => tasksInQueue
Note that in this system, if you did await[...]
on one of the promises returned by fetchResource(), you're going to cause all promises forever after to be rejected with a timeout error as well. You'll also make the tasksInQueue counter go out of sync - it'll just keep incrementing without ever decrementing. Why? Because the currentlyRunningTask promise was never supposed to reject. As it's currently coded, it can't reject. All errors get caught. Once you inject a rejection in there (which is currently impossible), then all of the promises that get tacked on will inherit that rejectedness, due to the way promises work. There may have been other ways to code this to get around this issue, but that's the thing, you have to code it with this await[...]
syntax in mind, at which point, you might as well just receive a cancel token and code your logic with a cancel token in mind instead.
Here's another issue. What if the async task creates another run-away async task, and doesn't await it? For example:
async function getInfo(id) {
await getResource(id)
sendAnalyticInformation(id)
}
The sendAnalyticInformation() is not awaited. It's fired and forgotten. It could hold up system resources forever. It might not even use promises, it could be a callback-based API.
There's one more issue. What if the timeout error gets caught? e.g. what if you're performing a query that tries to keep retrying, with an exponential back-off whenever an error occurs. This source of system would silently catch and ignore the timeout error, and just keep retrying.