GraphiteEditor/Graphite · error · syn::Error
command functions must be plain non-generic, non-async, safe
Error message
command functions must be plain non-generic, non-async, safe functions
What it means
Command functions must be plain, monomorphic, synchronous, safe functions. The macro generates a matching enum variant and dispatch code that has no way to instantiate generics, await futures, or uphold unsafe contracts, so generic params, `async`, or `unsafe` signatures are compile errors.
Source
Thrown at proc-macros/src/editor_commands.rs:55
return Err(Error::new(
attr.span(),
"command functions may not have attributes; anything that doesn't fit the `fn name(args…) -> Message` contract belongs in a plain impl block",
));
}
}
if !matches!(function.vis, Visibility::Inherited) {
return Err(Error::new(
function.span(),
"command functions have no visibility modifier; the macro generates the public JS-facing stub",
));
}
let signature = &function.sig;
if let Some(receiver) = signature.receiver() {
return Err(Error::new(receiver.span(), "command functions take no `self`; they are pure `args… -> Message` translations"));
}
if !signature.generics.params.is_empty() || signature.asyncness.is_some() || signature.unsafety.is_some() {
return Err(Error::new(signature.span(), "command functions must be plain non-generic, non-async, safe functions"));
}
let docs = &function.attrs;
let fn_name = &signature.ident;
let variant = Ident::new(&fn_name.to_string().to_case(Case::Pascal), fn_name.span());
let js_name = Ident::new(&fn_name.to_string().to_case(Case::Camel), fn_name.span());
let mut param_names = Vec::new();
let mut param_types = Vec::new();
for parameter in &signature.inputs {
let FnArg::Typed(pat_type) = parameter else { unreachable!("receiver is rejected above") };
let Pat::Ident(pat_ident) = &*pat_type.pat else {
return Err(Error::new(pat_type.span(), "command parameters must be plain identifiers"));
};
param_names.push(&pat_ident.ident);
param_types.push(&*pat_type.ty);
}
View on GitHub (pinned to c507b35645)
Solutions
- Remove `async`/`unsafe`/generic parameters and make the command a plain synchronous fn with concrete types.
- If generics were for code reuse, keep a generic helper in a plain module and have the non-generic command delegate to it.
- If async work is needed, make the command return the Message that schedules the work and run the async part in the editor's executor instead.
Example fix
// before
#[editor_commands]
mod commands {
async fn load_document(path: String) -> Message { ... }
fn set_value<T: Into<f64>>(v: T) -> Message { ... }
}
// after
#[editor_commands]
mod commands {
fn load_document(path: String) -> Message { ... }
fn set_value(v: f64) -> Message { ... }
} Defensive patterns
Strategy: validation
Prevention
- Keep commands synchronous, non-generic, and safe; do async work via returned Messages.
- Place generic helpers in a plain module and let the concrete command delegate to them.
When it happens
Trigger: Declaring a command with lifetime or type generics (`fn foo<T>(x: T) -> Message`), `async fn foo(…) -> Message`, `unsafe fn foo(…)`, or a where clause inside the #[editor_commands] module. The check rejects non-empty `sig.generics.params`, `asyncness`, or `unsafety`.
Common situations: Porting an async data-loading function into the command module, or trying to share one generic implementation across numeric parameter types. Also hit when adding a const generic for array lengths.
Related errors
- #[editor_commands] requires a module with an inline body
- only `use` imports and command functions may appear in an #[
- command functions may not have attributes; anything that doe
- command functions have no visibility modifier; the macro gen
- command functions take no `self`; they are pure `args… -> Me
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/ec710c225aa4c991.
Report an issue: GitHub.