vercel/next.js · critical · syn::Error

hash = "manual" only makes sense with serialization = "hash"

Error message

hash = "manual" only makes sense with serialization = "hash"

What it means

Compile-time error from the #[turbo_tasks::value] proc-macro. hash = "manual" tells the macro to use a hand-written Hash impl for the value type, but that only has meaning when serialization = "hash" (which is what triggers hash-based persistence). Using hash = "manual" with any other serialization mode (auto/custom/skip) is meaningless, so the macro rejects it.

Source

Thrown at turbopack/crates/turbo-tasks-macros/src/value_macro.rs:292

        operation,
        task_input,
    } = parse_macro_input!(args as ValueArguments);

    // `serialization = "hash"` only makes sense with `cell = "compare"` (the default).
    if matches!(serialization_mode, SerializationMode::Hash)
        && !matches!(cell_mode, CellMode::Compare)
    {
        return syn::Error::new(
            proc_macro2::Span::call_site(),
            "serialization = \"hash\" only makes sense with cell = \"compare\" (or default)",
        )
        .to_compile_error()
        .into();
    }

    // `hash = "manual"` only makes sense with `serialization = "hash"`.
    if manual_hash && !matches!(serialization_mode, SerializationMode::Hash) {
        return syn::Error::new(
            proc_macro2::Span::call_site(),
            "hash = \"manual\" only makes sense with serialization = \"hash\"",
        )
        .to_compile_error()
        .into();
    }

    // `evict = "last"` only makes sense for `serialization = "skip"`: it
    // says "re-deriving this cell is expensive", and re-derivation is the
    // recovery path only for skip mode. Persistable cells restore from disk
    // (predictable cost), HashOnly cells short-circuit on unchanged hash.
    //
    // `evict = "never"` is allowed with any serialization mode — a value
    // type can be persistable AND hold session-scoped state that must not
    // leave memory (e.g. `DiskFileSystem` carrying file watchers).
    if matches!(evict_mode, EvictMode::Last)
        && !matches!(serialization_mode, SerializationMode::Skip)
    {

View on GitHub (pinned to 0ae8c72462)

Solutions

  1. Set serialization = "hash" so the manual hash is actually used: #[turbo_tasks::value(serialization = "hash", hash = "manual")].
  2. Or remove hash = "manual" if you did not intend hash-based serialization.

Example fix

// before
#[turbo_tasks::value(serialization = "auto", hash = "manual")]
struct Foo { ... }

// after
#[turbo_tasks::value(serialization = "hash", hash = "manual")]
struct Foo { ... }
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Annotating a value type with hash = "manual" but a serialization mode other than "hash", e.g. #[turbo_tasks::value(serialization = "auto", hash = "manual")].

Common situations: A contributor enabling a manual Hash impl while forgetting to switch serialization to "hash"; leftover hash = "manual" after changing the serialization mode.

Related errors


AI-assisted analysis of vercel/next.js@0ae8c72462 (2026-08-06). Data as JSON: /api/errors/116af2c142a09651. Report an issue: GitHub.