diff --git a/.changes/improve-async-cmd-error-message.md b/.changes/improve-async-cmd-error-message.md
new file mode 100644
index 000000000..48bbbb02c
--- /dev/null
+++ b/.changes/improve-async-cmd-error-message.md
@@ -0,0 +1,5 @@
+---
+"tauri-macros": 'patch:enhance'
+---
+
+Improve compiler error message when generating an async command that has a reference input and don't return a Result.
diff --git a/core/tauri-macros/src/command/wrapper.rs b/core/tauri-macros/src/command/wrapper.rs
index f876fa0d4..008e0b8c7 100644
--- a/core/tauri-macros/src/command/wrapper.rs
+++ b/core/tauri-macros/src/command/wrapper.rs
@@ -5,7 +5,7 @@
use heck::{ToLowerCamelCase, ToSnakeCase};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, quote_spanned};
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
@@ -103,6 +103,63 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
resolver: format_ident!("__tauri_resolver__"),
};
+ // Tauri currently doesn't support async commands that take a reference as input and don't return
+ // a result. See: https://github.com/tauri-apps/tauri/issues/2533
+ //
+ // For now, we provide an informative error message to the user in that case. Once #2533 is
+ // resolved, this check can be removed.
+ let mut async_command_check = TokenStream2::new();
+ if function.sig.asyncness.is_some() {
+ // This check won't catch all possible problems but it should catch the most common ones.
+ let mut ref_argument_span = None;
+
+ for arg in &function.sig.inputs {
+ if let syn::FnArg::Typed(pat) = arg {
+ match &*pat.ty {
+ syn::Type::Reference(_) => {
+ ref_argument_span = Some(pat.span());
+ }
+ syn::Type::Path(path) => {
+ // Check if the type contains a lifetime argument
+ let last = path.path.segments.last().unwrap();
+ if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
+ if args
+ .args
+ .iter()
+ .any(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
+ {
+ ref_argument_span = Some(pat.span());
+ }
+ }
+ }
+ _ => {}
+ }
+
+ if let Some(span) = ref_argument_span {
+ if let syn::ReturnType::Type(_, return_type) = &function.sig.output {
+ // To check if the return type is `Result` we require it to check a trait that is
+ // only implemented by `Result`. That way we don't exclude renamed result types
+ // which we wouldn't otherwise be able to detect purely from the token stream.
+ // The "error message" displayed to the user is simply the trait name.
+ async_command_check = quote_spanned! {return_type.span() =>
+ #[allow(unreachable_code, clippy::diverging_sub_expression)]
+ const _: () = if false {
+ trait AsyncCommandMustReturnResult {}
+ impl AsyncCommandMustReturnResult for Result {}
+ let _check: #return_type = unreachable!();
+ let _: &dyn AsyncCommandMustReturnResult = &_check;
+ };
+ };
+ } else {
+ return quote_spanned! {
+ span => compile_error!("async commands that contain references as inputs must return a `Result`");
+ }.into();
+ }
+ }
+ }
+ }
+ }
+
// body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
let body = syn::parse::(attributes)
.map(|mut attrs| {
@@ -121,6 +178,8 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
// Rely on rust 2018 edition to allow importing a macro from a path.
quote!(
+ #async_command_check
+
#function
#maybe_macro_export