From 94e10aa6a1388dc8ee40366cc638f1c14db3b4a1 Mon Sep 17 00:00:00 2001 From: Ge Gao Date: Mon, 19 Oct 2026 06:33:51 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9E=20fix:=20Propagate=20asynchronous?= =?UTF-8?q?=20logger=20failures=20in=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++--- test/README.example.proof.ts | 4 +-- test/README.example.spec.ts | 70 ++++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 776b748..451e365 100644 --- a/README.md +++ b/README.md @@ -1562,9 +1562,9 @@ const logEffect = (level: LogLevel): EffectFactory => *defaultHandler({ resume }, ...args) { const logger = yield* askLogger(); const result = logger[level](...args); - // Handle async loggers - if (result instanceof Promise) result.then(resume); - else resume(); + // Wait for async loggers before resuming + if (result instanceof Promise) yield* effectify(result); + resume(); }, }); @@ -1575,7 +1575,7 @@ export const logWarn = logEffect("warn"); export const logError = logEffect("error"); ``` -We define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime. +We define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime. Asynchronous logger results are yielded through `effectify`, so `runAsync()` waits for them and rejects with the same error if the logger Promise rejects. **Step 3: Define handlers for common logging features** diff --git a/test/README.example.proof.ts b/test/README.example.proof.ts index 8280f7c..9eae93f 100644 --- a/test/README.example.proof.ts +++ b/test/README.example.proof.ts @@ -1134,8 +1134,8 @@ test("Example: Build a configurable logging system with effects", () => { *defaultHandler({ resume }, ...args) { const logger = yield* askLogger(); const result = logger[level](...args); - if (result instanceof Promise) void result.then(resume); - else resume(); + if (result instanceof Promise) yield* effectify(result); + resume(); }, }); diff --git a/test/README.example.spec.ts b/test/README.example.spec.ts index df10535..b19093c 100644 --- a/test/README.example.spec.ts +++ b/test/README.example.spec.ts @@ -1800,8 +1800,8 @@ test("Example: Build a configurable logging system with effects", async () => { *defaultHandler({ resume }, ...args) { const logger = yield* askLogger(); const result = logger[level](...args); - if (result instanceof Promise) void result.then(resume); - else resume(); + if (result instanceof Promise) yield* effectify(result); + resume(); }, }); @@ -1961,4 +1961,70 @@ test("Example: Build a configurable logging system with effects", async () => { ]); logs.length = 0; fileLogs.length = 0; + + // Synchronous loggers still support runSync. + const syncResult = effected(function* () { + yield* logInfo("Sync message"); + return "done"; + }).runSync(); + expect(syncResult).toBe("done"); + expect(logs).toEqual([["info", "Sync message"]]); + + // A pending logger must finish before the program resumes. + { + let finishWrite!: () => void; + const write = new Promise((resolve) => { + finishWrite = resolve; + }); + const logger = { ...mockConsole, info: vi.fn(() => write) }; + const continued = vi.fn(); + const result = effected(function* () { + yield* logInfo("Async message"); + continued(); + return "done"; + }) + .provide("logger", logger) + .runAsync(); + + expect(logger.info).toHaveBeenCalledExactlyOnceWith("Async message"); + expect(continued).not.toHaveBeenCalled(); + finishWrite(); + await expect(result).resolves.toBe("done"); + expect(continued).toHaveBeenCalledOnce(); + } + + // Logging failures must reject the public Promise without an unhandled derivative. + { + let failWrite!: (reason: Error) => void; + const write = new Promise((_, reject) => { + failWrite = reject; + }); + const failure = new Error("Logging failed"); + const logger = { ...mockConsole, info: vi.fn(() => write) }; + const continued = vi.fn(); + const unhandled = vi.fn(); + process.on("unhandledRejection", unhandled); + try { + const result = effected(function* () { + yield* logInfo("Failed message"); + continued(); + }) + .provide("logger", logger) + .runAsync(); + const fulfilled = vi.fn(); + const rejected = vi.fn(); + void result.then(fulfilled, rejected); + + expect(logger.info).toHaveBeenCalledExactlyOnceWith("Failed message"); + failWrite(failure); + await new Promise((resolve) => setImmediate(resolve)); + expect(rejected).toHaveBeenCalledOnce(); + expect(rejected.mock.calls[0]?.[0]).toBe(failure); + expect(fulfilled).not.toHaveBeenCalled(); + expect(continued).not.toHaveBeenCalled(); + expect(unhandled).not.toHaveBeenCalled(); + } finally { + process.off("unhandledRejection", unhandled); + } + } });