-
-
Notifications
You must be signed in to change notification settings - Fork 6
Open
Description
When the RPC provider fails (e.g., network error, 403 Forbidden, invalid API key), an unhandled promise rejection is thrown even when the error is properly caught in user code with try/catch.
Root Cause
drainCallQueue() is an async function called from setTimeout callbacks without awaiting or catching the returned promise:
Lines 66-68 (drainInterval setter):
this.#drainTimer = setTimeout(() => {
this.drainCallQueue() // async function - returned promise is ignored
}, this.#drainInterval);Lines 80-82 (queueCall):
this.#drainTimer = setTimeout(() => {
this.drainCallQueue(); // async function - returned promise is ignored
}, this.#drainInterval);When this.subprovider.call() fails inside drainCallQueue(), two issues occur:
- The rejection from
drainCallQueue()is unhandled (setTimeoutcallback doesn't catch it) - The queued promises from
queueCall()are never rejected because the error occurs before the resolve/reject logic runs
Reproduction
import { Contract, JsonRpcProvider } from 'ethers'
import { MulticallProvider } from '@ethers-ext/provider-multicall'
const provider = new JsonRpcProvider('https://invalid-rpc-url.example.com')
const multicallProvider = new MulticallProvider(provider)
const contract = new Contract('0x6B175474E89094C44Da98b954EedeAC495271d0F',
['function balanceOf(address) view returns (uint256)'],
multicallProvider)
try {
await contract.balanceOf('0x0000000000000000000000000000000000000001')
} catch (error) {
console.log('Caught:', error.message) // Error IS caught here
}
// But an unhandled rejection is ALSO printed to consoleProposed Fix
Wrap the subprovider.call() in try/catch inside the runner and properly reject all queued promises on error:
runners.push((async () => {
try {
const _data = await this.subprovider.call({ data, blockTag });
// ... existing success logic ...
} catch (error) {
callQueue.forEach(({ reject }) => reject(error));
}
})());This ensures:
- All queued promises are properly rejected on error
- Runners never throw, so
Promise.all(runners)always succeeds drainCallQueue()never rejects, eliminating the unhandled rejection
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels