From 8b7fedaf1899ce6ea62c4499f6d7b6dae6358bd4 Mon Sep 17 00:00:00 2001
From: Yuval Datner <22598347+datner@users.noreply.github.com>
Date: Mon, 4 May 2026 23:16:59 +0200
Subject: [PATCH 1/4] feat(effect): add `Effect.acquireDisposable`
---
packages/effect/src/Effect.ts | 23 ++++++++++++++++++++
packages/effect/src/internal/fiberRuntime.ts | 11 ++++++++++
2 files changed, 34 insertions(+)
diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts
index d9143c239b6..72b26918b35 100644
--- a/packages/effect/src/Effect.ts
+++ b/packages/effect/src/Effect.ts
@@ -5460,6 +5460,29 @@ export const acquireRelease: {
): Effect
} = fiberRuntime.acquireRelease
+/**
+ * Constructs a scoped resource from an {@linkcode AsyncDisposable} or {@linkcode Disposable}
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using}
+ *
+ * @example
+ * import sqlite from "node:sqlite";
+ * import { Effect } from "effect";
+ *
+ * // Define how the resource is acquired
+ * const acquire = Effect.sync(() => new sqlite.DatabaseSync(":memory:"))
+ * const resource = Effect.acquireDisposable(acquire)
+ *
+ * @see {@link acquireRelease} for more information about scopes.
+ *
+ * @since 3.0.0
+ * @category Scoping, Resources & Finalization
+ */
+export const acquireDisposable: {
+ (
+ acquire: Effect
+ ): Effect
+} = fiberRuntime.acquireDisposable
+
/**
* Creates a scoped resource with an interruptible acquire action.
*
diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts
index f93e75c92f9..47b399dd80e 100644
--- a/packages/effect/src/internal/fiberRuntime.ts
+++ b/packages/effect/src/internal/fiberRuntime.ts
@@ -1691,6 +1691,17 @@ export const acquireRelease: {
core.tap(acquire, (a) => addFinalizer((exit) => release(a, exit)))
))
+/* @internal */
+export const acquireDisposable: {
+ (
+ acquire: Effect.Effect
+ ): Effect.Effect
+} = (acquire) =>
+ acquireRelease(acquire, (resource) =>
+ Predicate.hasProperty(resource, Symbol.asyncDispose)
+ ? internalEffect.promise(() => resource[Symbol.asyncDispose]())
+ : core.sync(() => resource[Symbol.dispose]()))
+
/* @internal */
export const acquireReleaseInterruptible: {
(
From 8d28ff301ddd819dfe7e9cd08fe7f616e20c3465 Mon Sep 17 00:00:00 2001
From: Yuval Datner <22598347+datner@users.noreply.github.com>
Date: Mon, 4 May 2026 23:18:32 +0200
Subject: [PATCH 2/4] test(effect): happy path and failure for
`Effect.acquireDisposable`
---
.../test/Effect/acquire-release.test.ts | 68 ++++++++++++++++++-
1 file changed, 67 insertions(+), 1 deletion(-)
diff --git a/packages/effect/test/Effect/acquire-release.test.ts b/packages/effect/test/Effect/acquire-release.test.ts
index 575b8923ba9..42c6d25d5d1 100644
--- a/packages/effect/test/Effect/acquire-release.test.ts
+++ b/packages/effect/test/Effect/acquire-release.test.ts
@@ -1,5 +1,5 @@
import { describe, it } from "@effect/vitest"
-import { assertTrue, strictEqual } from "@effect/vitest/utils"
+import { assertEquals, assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils"
import * as Cause from "effect/Cause"
import * as Chunk from "effect/Chunk"
import * as Effect from "effect/Effect"
@@ -8,6 +8,17 @@ import * as Exit from "effect/Exit"
import { pipe } from "effect/Function"
import * as Ref from "effect/Ref"
+const disposable = (hook: () => void) => ({
+ [Symbol.dispose]() {
+ hook()
+ }
+})
+const asyncDisposable = (hook: () => Promise) => ({
+ async [Symbol.asyncDispose]() {
+ await hook()
+ }
+})
+
describe("Effect", () => {
it.effect("acquireUseRelease - happy path", () =>
Effect.gen(function*() {
@@ -108,4 +119,59 @@ describe("Effect", () => {
assertTrue(equals(Cause.defects(result), Chunk.of(useDied)))
assertTrue(released)
}))
+ it.effect("acquireDisposable - happy path", () =>
+ Effect.gen(function*() {
+ let disposed = false
+ yield* Effect.succeed(
+ disposable(() => {
+ disposed = true
+ })
+ )
+ .pipe(
+ Effect.acquireDisposable,
+ Effect.tap(() => assertFalse(disposed)),
+ Effect.scoped
+ )
+ assertTrue(disposed)
+ }))
+ it.effect("acquireDisposable - happy path async", () =>
+ Effect.gen(function*() {
+ let disposed = false
+ yield* Effect.succeed(
+ asyncDisposable(() =>
+ new Promise((resolve) => {
+ disposed = true
+ resolve()
+ })
+ )
+ )
+ .pipe(
+ Effect.acquireDisposable,
+ Effect.tap(() => assertFalse(disposed)),
+ Effect.scoped
+ )
+ assertTrue(disposed)
+ }))
+ it.effect("acquireDisposable - error handling", () =>
+ Effect.gen(function*() {
+ const err = new Error("oh no!")
+ const exit = yield* Effect.succeed(
+ disposable(() => {
+ throw err
+ })
+ )
+ .pipe(
+ Effect.acquireDisposable,
+ Effect.scoped,
+ Effect.exit
+ )
+ const result = yield* Exit.matchEffect(
+ exit,
+ {
+ onFailure: Effect.succeed,
+ onSuccess: () => Effect.fail("effect should have failed")
+ }
+ )
+ assertEquals(Cause.defects(result), Chunk.of(err))
+ }))
})
From e3bca390a130cca9fb25fd4e6f6242c3498c5628 Mon Sep 17 00:00:00 2001
From: Yuval Datner <22598347+datner@users.noreply.github.com>
Date: Mon, 4 May 2026 23:20:03 +0200
Subject: [PATCH 3/4] test(effect): change `assertTrue(equals` to
`assertEquals`
I would've used `strictEquals` but it does not use the `Equals` trait
despite claiming it does
---
packages/effect/test/Effect/acquire-release.test.ts | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/packages/effect/test/Effect/acquire-release.test.ts b/packages/effect/test/Effect/acquire-release.test.ts
index 42c6d25d5d1..b01ed90d7a7 100644
--- a/packages/effect/test/Effect/acquire-release.test.ts
+++ b/packages/effect/test/Effect/acquire-release.test.ts
@@ -3,7 +3,6 @@ import { assertEquals, assertFalse, assertTrue, strictEqual } from "@effect/vite
import * as Cause from "effect/Cause"
import * as Chunk from "effect/Chunk"
import * as Effect from "effect/Effect"
-import { equals } from "effect/Equal"
import * as Exit from "effect/Exit"
import { pipe } from "effect/Function"
import * as Ref from "effect/Ref"
@@ -64,8 +63,8 @@ describe("Effect", () => {
exit,
Exit.matchEffect({ onFailure: Effect.succeed, onSuccess: () => Effect.fail("effect should have failed") })
)
- assertTrue(equals(Cause.failures(result), Chunk.of("use failed")))
- assertTrue(equals(Cause.defects(result), Chunk.of(releaseDied)))
+ assertEquals(Cause.failures(result), Chunk.of("use failed"))
+ assertEquals(Cause.defects(result), Chunk.of(releaseDied))
}))
it.effect("acquireUseRelease - error handling + disconnect", () =>
Effect.gen(function*() {
@@ -86,8 +85,8 @@ describe("Effect", () => {
onSuccess: () => Effect.fail("effect should have failed")
})
)
- assertTrue(equals(Cause.failures(result), Chunk.of("use failed")))
- assertTrue(equals(Cause.defects(result), Chunk.of(releaseDied)))
+ assertEquals(Cause.failures(result), Chunk.of("use failed"))
+ assertEquals(Cause.defects(result), Chunk.of(releaseDied))
}))
it.effect("acquireUseRelease - beast mode error handling + disconnect", () =>
Effect.gen(function*() {
@@ -116,7 +115,7 @@ describe("Effect", () => {
)
)
const released = yield* (Ref.get(release))
- assertTrue(equals(Cause.defects(result), Chunk.of(useDied)))
+ assertEquals(Cause.defects(result), Chunk.of(useDied))
assertTrue(released)
}))
it.effect("acquireDisposable - happy path", () =>
From 27ba280c54658bdaa72980fec90edbb7e5015cbb Mon Sep 17 00:00:00 2001
From: Yuval Datner <22598347+datner@users.noreply.github.com>
Date: Mon, 4 May 2026 23:20:20 +0200
Subject: [PATCH 4/4] chore: add changeset
---
.changeset/big-steaks-train.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/big-steaks-train.md
diff --git a/.changeset/big-steaks-train.md b/.changeset/big-steaks-train.md
new file mode 100644
index 00000000000..dd3d3c2a0b9
--- /dev/null
+++ b/.changeset/big-steaks-train.md
@@ -0,0 +1,5 @@
+---
+"effect": minor
+---
+
+add `Effect.acquireDisposable`