You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
@std/async/unstable-semaphore should be bind methods in the constructor #7195
error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '#head')
if (this.#head) {
^
at release (https://jsr.io/@std/async/1.4.0/unstable_semaphore.ts:139:14)
at <anonymous>
however this somehow worked(?) but inconsistently. so it should be changed, by binding in the constructor this.release = this.release.bind(this), or by fixing whatever makes it inconsistently work. I have no idea why it sometimes works. Seems to have a higher chance of working when the semaphore max is higher, but it works even when the total number of acquire/release calls is more than max so idk.
The throw is deterministic, not flaky. release reads this.#head on its first line, and .finally(sema.release) invokes it with this === undefined, so it always throws TypeError: Cannot read properties of undefined (reading '#head').
What varies is the consequence, which is what made it look inconsistent:
The rejection is unobserved (no .catch), so whether you actually see "Uncaught (in promise)" depends on whether the runtime surfaces the unhandled rejection before the program exits.
Every detached release throws before restoring the permit, so the permit leaks. Starting from max, each leaked permit shrinks the live pool, and a later acquire only blocks once cumulative leaks exhaust the headroom — so a higher max masks it longer, exactly as you saw.
Permits never recover:
consts=newSemaphore(2);awaits.acquire().then(()=>{}).finally(s.release).catch(()=>{});awaits.acquire().then(()=>{}).finally(s.release).catch(()=>{});// both permits leaked; the next acquire() blocks forever
On the fix: binding in the constructor works, but there's no *.bind(this) in a constructor anywhere in std today. An arrow class field (release = (): void => { ... }) auto-binds, or release stays a prototype method and the doc notes it must be called on the instance. Since this is a concurrency primitive the per-instance allocation tradeoff is a real one. Happy to send a PR for whichever shape you'd prefer.
Activity
truffle-dev commentedon Jun 29, 2026
The throw is deterministic, not flaky.
releasereadsthis.#headon its first line, and.finally(sema.release)invokes it withthis === undefined, so it always throwsTypeError: Cannot read properties of undefined (reading '#head').What varies is the consequence, which is what made it look inconsistent:
.catch), so whether you actually see "Uncaught (in promise)" depends on whether the runtime surfaces the unhandled rejection before the program exits.releasethrows before restoring the permit, so the permit leaks. Starting frommax, each leaked permit shrinks the live pool, and a lateracquireonly blocks once cumulative leaks exhaust the headroom — so a highermaxmasks it longer, exactly as you saw.Permits never recover:
On the fix: binding in the constructor works, but there's no
*.bind(this)in a constructor anywhere in std today. An arrow class field (release = (): void => { ... }) auto-binds, orreleasestays a prototype method and the doc notes it must be called on the instance. Since this is a concurrency primitive the per-instance allocation tradeoff is a real one. Happy to send a PR for whichever shape you'd prefer.fix(async/unstable-semaphore): bind methods in constructor (denoland#…