Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migration Guide

Stillwater 1.x to 2.0

The core Effect trait remains asynchronous. This release removes deprecated compatibility APIs and gives effect traversal explicit execution names.

Choose traversal semantics deliberately

1.x APIPreserve concurrent executionOpt into ordered, fail-fast execution
traverse_effect(items, f)traverse_effect_parallel(items, f)traverse_effect_sequential(items, f)
sequence_effect(effects)sequence_effect_parallel(effects)sequence_effect_sequential(effects)

The 1.x implementation ran children concurrently and awaited all results, despite documentation claiming sequential fail-fast behavior. The parallel variants preserve that execution policy. Choosing sequential is an intentional behavior change: later operations will not run after an earlier failure.

Traversal factory timing also changes. In 1.x, traverse_effect called f immediately when building the traversal. Both new variants defer f until execution; parallel constructs the full batch before polling children, while sequential constructs each child only after the previous one succeeds. Move required construction-time work into an explicit step rather than relying on the old factory timing.

All variants still eagerly enumerate their input iterators when called. They clone the environment for execution and return boxed effects. Concurrent completion order does not change output ordering or which error is returned: the first error by input position wins after all children finish. Dropping the parent future cancels unfinished children.

use stillwater::prelude::*;

tokio_test::block_on(async {
    let batch = traverse_effect_parallel([1, 2, 3], |value| {
        pure::<_, &str, ()>(value * 2).boxed()
    });
    assert_eq!(batch.run(&()).await, Ok(vec![2, 4, 6]));

    let ordered = sequence_effect_sequential(vec![
        pure::<_, &str, ()>(1).boxed(),
        fail("stop").boxed(),
        pure(3).boxed(),
    ]);
    assert_eq!(ordered.run(&()).await, Err("stop"));
});

Replace legacy aliases and constructors

LegacyEffect and LegacyConstructors have been removed, not just deprecated. Use free constructors, return impl Effect for concrete composition, and use BoxedEffect when a single erased type is needed. RunStandalone remains supported.

use stillwater::prelude::*;

fn concrete() -> impl Effect<Output = i32, Error = &'static str, Env = ()> {
    pure(42)
}

fn erased() -> BoxedEffect<i32, &'static str, ()> {
    concrete().boxed()
}

tokio_test::block_on(async {
    assert_eq!(concrete().run_standalone().await, Ok(42));
    assert_eq!(erased().run(&()).await, Ok(42));
});

Replace bracket_simple

The removed function accepted (acquire, use_fn, release_fn). The replacement bracket accepts (acquire, release_fn, use_fn).

The use callback now receives &R; release receives ownership of R and returns a future with output Result<(), E>. It is an async callback, not an Effect. The error type also needs Debug. If use needs owned data, explicitly clone the necessary data before constructing its effect; the resource itself no longer needs Clone just to enter the use callback.

use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use stillwater::prelude::*;

tokio_test::block_on(async {
    let released = Arc::new(AtomicBool::new(false));
    let observed = Arc::clone(&released);
    let effect = bracket(
        pure::<_, &str, ()>(String::from("connection")),
        move |_resource| async move {
            observed.store(true, Ordering::SeqCst);
            Ok(())
        },
        |resource: &String| pure(resource.len()),
    );
    assert_eq!(effect.run(&()).await, Ok(10));
    assert!(released.load(Ordering::SeqCst));
});

bracket logs cleanup errors and returns the use result. Choose bracket_full when cleanup failure must be returned to the caller:

use stillwater::prelude::*;

tokio_test::block_on(async {
    let effect = bracket_full(
        pure::<_, &str, ()>(()),
        |_| async { Err("cleanup failed") },
        |_| pure(42),
    );
    assert_eq!(
        effect.run(&()).await,
        Err(BracketError::CleanupError("cleanup failed")),
    );
});

These async brackets do not mask cancellation or guarantee cleanup after panic. Use normal ownership/RAII where possible and design asynchronous cleanup protocols explicitly.

Features and allocation expectations

The async feature enables Tokio-backed retry and timeout helpers; it does not switch Effect between sync and async modes. IO keeps its existing infallible outer error channel and is not a replacement for fallible Effect constructors.

Most combinators store concrete fields without adding boxes. from_async_ref accepts a boxed borrowed future; IO, traversal, and retry helpers return boxed effects. Calls to .boxed() are therefore not the only possible allocation sites.

Upgrading directly from pre-0.11 releases

Those releases used an Effect<T, E, Env> struct and associated constructors. Use the current replacements above rather than migrating through removed aliases.

Historical spellingCurrent spelling
Effect::pure(value)pure(value)
Effect::fail(error)fail(error)
Effect::from_fn(f)from_fn(f)
Effect<T, E, Env>impl Effect<Output = T, Error = E, Env = Env> or BoxedEffect<T, E, Env>

Use owned or borrowed async constructors according to whether the returned future borrows its environment. See the API tiers for the recommended 2.0 surface.