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
- Set serialization = "hash" so the manual hash is actually used: #[turbo_tasks::value(serialization = "hash", hash = "manual")].
- 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
- Only set hash = "manual" together with serialization = "hash".
- If you change serialization away from "hash", remove any hash = "manual" attribute in the same edit.
- Treat the macro's attribute set as coupled: hash-manual implies serialization-hash.
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
- serialization = "hash" only makes sense with cell = "compare
- evict = "last" is only valid with serialization = "skip"
- gave up applying effects after {MAX_RETRIES} retries; repeat
AI-assisted analysis of vercel/next.js@0ae8c72462 (2026-08-06).
Data as JSON: /api/errors/116af2c142a09651.
Report an issue: GitHub.