Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

v4 Cache.get: a caller that joins an abandoned lookup fails with an interruption it did not ask for

Open
#9,693 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
42/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
api, backend

Research direction

The report points to Cache.get and EntryImpl.await; start by tracing how callers join an entry, how awaiters is updated, and how the lookup fiber or Deferred completes. Reproduce the timing in the issue’s TypeScript example, then compare the behavior on the v4 branch and main. Done means a caller that did not leave receives the lookup’s result or its own failure, not another caller’s interruption.

Written by the indexing model from the issue text.

Description

Version: effect 4.0.0-beta.107 (the v4 line developed in Effect-TS/effect-smol, which does not take issues from outside the org).

Cache.get can fail a caller with an interrupt-only cause even though nothing interrupted that caller. It happens when the caller joins a lookup that another caller abandoned.

Reproduction (effect 4.0.0-beta.107, Bun 1.4.3)
import { Cache, Cause, Effect, Exit, Fiber } from "effect"

const program = Effect.gen(function* () {
  let lookups = 0
  const cache = yield* Cache.make({
    capacity: 10,
    // A lookup that cannot stop at once, like a spawned process or a blocked query
    lookup: (_key: string) =>
      Effect.sync(() => ++lookups).pipe(
        Effect.andThen(Effect.sleep("300 millis").pipe(Effect.uninterruptible)),
        Effect.as("value")
      )
  })
  const a = yield* Cache.get(cache, "k").pipe(Effect.forkChild)
  yield* Effect.sleep("50 millis")
  yield* Fiber.interrupt(a).pipe(Effect.forkChild) // the only waiting caller gives up
  yield* Effect.sleep("50 millis")
  const b = yield* Effect.exit(Cache.get(cache, "k")) // b is never interrupted
  console.log(Exit.isFailure(b) && Cause.hasInterruptsOnly(b.cause)) // true
  const c = yield* Effect.exit(Cache.get(cache, "k"))
  console.log(Exit.isSuccess(c), lookups) // true 2
})

Effect.runPromise(program)
What happens

When the last caller waiting on an entry leaves, the cache interrupts the lookup fiber (in 4.0.0-beta.107, EntryImpl.await calls fiberInterrupt once awaiters reaches 0). A caller that calls get before that fiber has exited finds the entry still in the map. It joins the fiber and receives its interrupted exit. Any catchCause above that caller then sees an interrupt-only cause, which looks exactly like a cancellation of the caller itself.

On main the lookup runs inside the first caller's fiber (effect.onExit(self.lookup(key), …) completes the shared Deferred with that fiber's exit). Interrupting that first caller would complete the Deferred with its interruption for every joiner. That conclusion comes from reading the code; I have not run it.

Expected

A caller that was not interrupted should get the lookup's value, or a failure of the lookup's own. One way to get there: the lookup is owned by the cache rather than by its first caller, and a caller that leaves stops only its own wait.

Impact

We hit this in a desktop app: a request that joined a repository lookup, which a timed-out snapshot had abandoned, ended interrupted. That stopped the user's first message from starting a session until they sent a second one. Thanks to @dahli4, whose trace from their own machine pinned it down. On our side we now run lookups in a scope the cache's owner holds, and retry a joiner that got an interruption it did not ask for.

Dominant language
TypeScript
Stars
16.7k
Forks
808
Avg merge
10h 7m
Merged PRs (30d)
808

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Effect-TS/effect

All issues in Effect-TS/effect

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.