rust-lang / rust-lang/rust

Confusing diagnostic: async closure type mismatch

Open
#148,260 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-diagnostics T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Code
use std::{
    any::{Any, TypeId},
    hash::{DefaultHasher, Hash, Hasher},
    sync::Arc,
    time::{Duration, Instant},
};

#[tokio::main]
async fn main() {
    myfunc(&GrpcCache::default(), &Client::default()).await;
}

#[derive(Default)]
struct Client {
    // Placeholder
}

impl Client {
    async fn do_query(&self, query: &Query) -> Result<String, tonic::Status> {
        // Placeholder implementation, really this is actually a tonic GRPC client
        Ok(query.value.to_string())
    }
}

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
struct Query {
    // Placeholder
    value: u32,
}

async fn myfunc(cache: &GrpcCache, client: &Client) {
    let runner = async move |query: &Query| client.do_query(query).await;

    let grpc_response = cache
        .query_with_cache(
            Box::new(Query { value: 42 }),
            Duration::from_secs(30),
            runner,
        )
        .await;

    todo!()
}

type CacheKey = Box<dyn DynKey>;
type TypeErasedValue = Arc<dyn Any + Send + Sync>;
type CacheValue = (std::time::Duration, Result<TypeErasedValue, tonic::Status>);

/// A cache for queries
#[derive(Debug)]
pub struct GrpcCache {
    /// Generic cache for requests to responses
    cache: moka::future::Cache<CacheKey, CacheValue>,
}

impl Default for GrpcCache {
    fn default() -> Self {
        Self {
            cache: moka::future::Cache::builder()
                .max_capacity(1024)
                .initial_capacity(1024)
                .expire_after(ExpiryPolicy)
                .build(),
        }
    }
}

impl GrpcCache {
    /// Execute a GRPC query with caching.
    ///
    /// The actual resolving function is provided as a FnOnce closure,
    /// which will be called if the response was not cached.
    // Allow trivial casts, casting to dyn Any is trivial but needed to guide the type system.
    pub async fn query_with_cache<Query, F, Response, Fut>(
        &self,
        query: Box<Query>,
        ttl: Duration,
        f: F,
    ) -> std::result::Result<Arc<Response>, tonic::Status>
    where
        Query: DynKey + std::fmt::Debug,
        F: FnOnce(&Query) -> Fut,
        Fut: std::future::Future<Output = std::result::Result<Response, tonic::Status>>,
        Response: Any + Clone + Send + Sync + 'static,
    {
        let query = query as Box<dyn DynKey>;
        let cache_lookup = self.cache.get(&query).await;
        if let Some((_, value)) = cache_lookup {
            match value {
                Ok(response) => match response.downcast::<Response>() {
                    Ok(value) => {
                        return Ok(value);
                    }
                    Err(_) => {
                        return Err(tonic::Status::internal(
                            "Cache hit with invalid unexpected type",
                        ));
                    }
                },
                Err(status) => return Err(status.clone()),
            }
        }

        // Not in cache, run the query
        // Hilariously, since the Box<dyn DynKey> cast above took ownership, we need to
        // cast it back to the known concrete type:
        let query = (query as Box<dyn Any + 'static>)
            .downcast::<Query>()
            .expect("Internal error: we just cast this...");

        let resp = f(&*query).await;
        let resp = resp.map(|r| r);
        let resp = resp.map(|inner| Arc::new(inner));
        let dyn_resp = resp.clone().map(|inner| inner as TypeErasedValue);
        self.cache
            .insert(query as Box<dyn DynKey>, (ttl, dyn_resp))
            .await;
        resp
    }
}

/// A key for a type-erased hashmap or cache.
///
/// Based on https://stackoverflow.com/questions/64838355/how-do-i-create-a-hashmap-with-type-erased-keys but with
/// improvements that are possible after 5 years of Rust development.
pub trait DynKey: Any + Send + Sync {
    fn eq(&self, other: &dyn DynKey) -> bool;
    fn hash(&self) -> u64;
    fn type_name(&self) -> &'static str;
}

impl<T: Eq + Send + Sync + Hash + 'static> DynKey for T {
    fn eq(&self, other: &dyn DynKey) -> bool {
        #[allow(trivial_casts)]
        if let Some(other) = (other as &dyn Any).downcast_ref::<T>() {
            return self == other;
        }
        false
    }

    fn hash(&self) -> u64 {
        let mut h = DefaultHasher::new();
        // mix the typeid of T into the hash to make distinct types
        // provide distinct hashes
        Hash::hash(&(TypeId::of::<T>(), self), &mut h);
        h.finish()
    }

    fn type_name(&self) -> &'static str {
        std::any::type_name::<T>()
    }
}

impl PartialEq for Box<dyn DynKey> {
    fn eq(&self, other: &Self) -> bool {
        DynKey::eq(self.as_ref(), other.as_ref())
    }
}

impl Eq for Box<dyn DynKey> {}

impl Hash for Box<dyn DynKey> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        let key_hash = DynKey::hash(self.as_ref());
        state.write_u64(key_hash);
    }
}

impl std::fmt::Debug for Box<dyn DynKey> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DynKey")
            .field("type", &self.type_name())
            .finish_non_exhaustive()
    }
}

/// Expiry policy for use with moka
struct ExpiryPolicy;

impl moka::Expiry<CacheKey, CacheValue> for ExpiryPolicy {
    fn expire_after_create(
        &self,
        _key: &CacheKey,
        value: &CacheValue,
        _created_at: Instant,
    ) -> Option<Duration> {
        Some(value.0)
    }
}
Current output
error[E0308]: mismatched types
  --> src/main.rs:37:25
   |
32 |       let runner = async move |query: &Query| client.do_query(query).await;
   |                                               ----------------------------
   |                                               |
   |                                               the expected `async` closure body
   |                                               the found `async` closure body
...
37 |       let grpc_response = cache
   |  _________________________^
38 | |         .query_with_cache(
39 | |             Box::new(Query { value: 42 }),
40 | |             Duration::from_secs(30),
41 | |             runner,
42 | |         )
   | |_________^ one type is more general than the other
   |
   = note: expected `async` closure body `{async closure body@src/main.rs:32:45: 32:73}`
              found `async` closure body `{async closure body@src/main.rs:32:45: 32:73}`
   = note: no two async blocks, even if identical, have the same type
   = help: consider pinning your async block and casting it to a trait object
note: the lifetime requirement is introduced here
  --> src/main.rs:87:30
   |
87 |         F: FnOnce(&Query) -> Fut,
   |                              ^^^

error[E0308]: mismatched types
  --> src/main.rs:43:10
   |
32 |     let runner = async move |query: &Query| client.do_query(query).await;
   |                                             ----------------------------
   |                                             |
   |                                             the expected `async` closure body
   |                                             the found `async` closure body
...
43 |         .await;
   |          ^^^^^ one type is more general than the other
   |
   = note: expected `async` closure body `{async closure body@src/main.rs:32:45: 32:73}`
              found `async` closure body `{async closure body@src/main.rs:32:45: 32:73}`
   = note: no two async blocks, even if identical, have the same type
   = help: consider pinning your async block and casting it to a trait object
note: the lifetime requirement is introduced here
  --> src/main.rs:87:30
   |
87 |         F: FnOnce(&Query) -> Fut,
   |                              ^^^

For more information about this error, try `rustc --explain E0308`.
warning: `async_reproducer` (bin "async_reproducer") generated 1 warning
error: could not compile `async_reproducer` (bin "async_reproducer") due to 2 previous errors; 1 warning emitted
Desired output
A hint that I should use `AsyncFnOnce(...) -> T` rather than `FnOnce(...) -> Future<Output = T>`.
Rationale and extra context

The current diagnostic is extremely obtuse, and doesn't explain what the issue is (at least to mere mortals). It would be much better to have a hint that the fix is to change query_with_cache to take a impl AsyncFnOnce(...) -> T.

Other cases

Rust Version
❯ rustc --version --verbose
rustc 1.90.0 (1159e78c4 2025-09-14)
binary: rustc
commit-hash: 1159e78c4747b02ef996e55082b704c09b970588
commit-date: 2025-09-14
host: x86_64-unknown-linux-gnu
release: 1.90.0
LLVM version: 20.1.8
Anything else?

First reported on URLO: https://users.rust-lang.org/t/very-confusing-async-error-async-closure-is-not-the-same-as-itself/134971

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the error from the example in src/main.rs, especially the async closure at line 32 and the generic bound at line 87. Start with the E0308 diagnostic and rustc --explain E0308, then locate the compiler diagnostic tests for this async-closure lifetime mismatch. Done means the error points users toward AsyncFnOnce rather than only reporting identical async closure body types.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.