diff --git a/.changeset/poor-fans-change.md b/.changeset/poor-fans-change.md
new file mode 100644
index 0000000000..18cd474157
--- /dev/null
+++ b/.changeset/poor-fans-change.md
@@ -0,0 +1,18 @@
+---
+"effect": minor
+---
+
+add Effect.timeoutOption
+
+Returns an effect that will return `None` if the effect times out, otherwise it
+will return `Some` of the produced value.
+
+```ts
+import { Effect } from "effect"
+
+// will return `None` after 500 millis
+Effect.succeed("hello").pipe(
+ Effect.delay(1000),
+ Effect.timeoutOption("500 millis")
+)
+```
diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts
index be87c87c6e..0b748b1c8e 100644
--- a/packages/effect/src/Effect.ts
+++ b/packages/effect/src/Effect.ts
@@ -2932,9 +2932,8 @@ export const timedWith: {
} = effect.timedWith
/**
- * Returns an effect that will timeout this effect, returning `None` if the
- * timeout elapses before the effect has produced a value; and returning
- * `Some` of the produced value otherwise.
+ * Returns an effect that will timeout this effect, failing with a `Cause.TimeoutException`
+ * if the timeout elapses before the effect has produced a value.
*
* If the timeout elapses without producing a value, the running effect will
* be safely interrupted.
@@ -2955,6 +2954,30 @@ export const timeout: {
(self: Effect, duration: Duration.DurationInput): Effect
} = circular.timeout
+/**
+ * Returns an effect that will timeout this effect, returning `None` if the
+ * timeout elapses before the effect has produced a value; and returning
+ * `Some` of the produced value otherwise.
+ *
+ * If the timeout elapses without producing a value, the running effect will
+ * be safely interrupted.
+ *
+ * WARNING: The effect returned by this method will not itself return until
+ * the underlying effect is actually interrupted. This leads to more
+ * predictable resource utilization. If early return is desired, then instead
+ * of using `effect.timeout(d)`, use `effect.disconnect.timeout(d)`, which
+ * first disconnects the effect's interruption signal before performing the
+ * timeout, resulting in earliest possible return, before an underlying effect
+ * has been successfully interrupted.
+ *
+ * @since 3.1.0
+ * @category delays & timeouts
+ */
+export const timeoutOption: {
+ (duration: Duration.DurationInput): (self: Effect) => Effect, E, R>
+ (self: Effect, duration: Duration.DurationInput): Effect, E, R>
+} = circular.timeoutOption
+
/**
* The same as `timeout`, but instead of producing a `None` in the event of
* timeout, it will produce the specified error.
diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts
index 1bfcb66712..d98904a53b 100644
--- a/packages/effect/src/internal/effect/circular.ts
+++ b/packages/effect/src/internal/effect/circular.ts
@@ -467,6 +467,22 @@ export const timeoutFailCause = dual<
duration
})))
+/** @internal */
+export const timeoutOption = dual<
+ (
+ duration: Duration.DurationInput
+ ) => (self: Effect.Effect) => Effect.Effect, E, R>,
+ (
+ self: Effect.Effect,
+ duration: Duration.DurationInput
+ ) => Effect.Effect, E, R>
+>(2, (self, duration) =>
+ timeoutTo(self, {
+ duration,
+ onSuccess: Option.some,
+ onTimeout: Option.none
+ }))
+
/** @internal */
export const timeoutTo = dual<
(