Description
lazy() in packages/core/src/util/lazy.ts sets loaded = true before calling fn():
return (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
If fn() throws, loaded is already true but value was never assigned. Every subsequent call then takes the if (loaded) fast path and silently returns undefined forever — the initializer never runs again, and the original error is swallowed.
That turns a transient failure into a permanent one, and replaces a clear error with a confusing undefined downstream.
Steps to reproduce
The synchronous lazy() callers that can throw are affected. Using the shape of packages/enterprise/src/core/storage.ts:86:
import { lazy } from "./src/util/lazy"
const adapter = lazy(() => {
const type = process.env["OPENCODE_STORAGE_ADAPTER"]
if (type === "s3") return { read: () => "s3" }
throw new Error("No storage adapter configured")
})
try { adapter() } catch (e) { console.log("call 1:", e.message) }
process.env["OPENCODE_STORAGE_ADAPTER"] = "s3"
console.log("call 2:", adapter().read())
Actual:
call 1: No storage adapter configured
TypeError: undefined is not an object (evaluating 'adapter().read')
Expected: call 2 either succeeds, or at minimum repeats the real No storage adapter configured error instead of degrading into a TypeError about undefined.
Other synchronous callers with the same exposure: packages/opencode/src/server/server.ts:56, packages/opencode/src/server/routes/instance/httpapi/server.ts:188.
OS
Windows (behaviour is platform-independent — it is pure logic in the memoization guard)
Description
lazy()inpackages/core/src/util/lazy.tssetsloaded = truebefore callingfn():If
fn()throws,loadedis alreadytruebutvaluewas never assigned. Every subsequent call then takes theif (loaded)fast path and silently returnsundefinedforever — the initializer never runs again, and the original error is swallowed.That turns a transient failure into a permanent one, and replaces a clear error with a confusing
undefineddownstream.Steps to reproduce
The synchronous
lazy()callers that can throw are affected. Using the shape ofpackages/enterprise/src/core/storage.ts:86:Actual:
Expected: call 2 either succeeds, or at minimum repeats the real
No storage adapter configurederror instead of degrading into aTypeErroraboutundefined.Other synchronous callers with the same exposure:
packages/opencode/src/server/server.ts:56,packages/opencode/src/server/routes/instance/httpapi/server.ts:188.OS
Windows (behaviour is platform-independent — it is pure logic in the memoization guard)