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

  1. Remove `async`/`unsafe`/generic parameters and make the command a plain synchronous fn with concrete types.
  2. If generics were for code reuse, keep a generic helper in a plain module and have the non-generic command delegate to it.
  3. 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

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


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/ec710c225aa4c991. Report an issue: GitHub.