Generic Data Types and `std::fmt::Debug`

Hi, I'm using aws_sdk_dynamodb 0.15.0 and try to handle errors correctly.

This small snippet actually compiles:

async fn success(client: aws_sdk_dynamodb::client::Client) {
    let result = client.query().send().await;

    if let Err(e) = result {
        eprintln!("DynamoDB error {:?}",e);
    }
}

The type of e is aws_sdk_dynamodb::types::SdkError and compiler is happy.

Next, I try to create a function that will display the error, and did this:

fn report_error<E>(e: aws_sdk_dynamodb::types::SdkError<E>) {
    eprintln!("DynamoDB error: {:?}", e);
}

async fn fail(client: aws_sdk_dynamodb::client::Client) {
    let result = client.query().send().await;

    if let Err(e) = result {
        report_error(e);
    }
}

What I want is just move the eprintln() into a separated function. But the compiler tell me:
E cannot be formatted using {:?} because it doesn't implement std::fmt::Debug

The type of e is the same, right? and it actually implement the trait Debug.

I don't get it.

I'm not sure what you are asking here. Same as what?

But nothing in the signature of report_error declares that. You have to add the E: Debug bound on your E type parameter.

derive(Debug), which aws_sdk_dynamodb::types::SdkError<E> uses, requires any generic types on the type to also implement Debug for the generated impl. That's more conservative than it always needs to be, but generally reliable (and correct in this case).

Basically, the impl will look like

impl<E> Debug for SdkError<E>
where
    E: Debug
{
   // ...
}

The difference in your code is report_error can't see that aws_sdk_dynamodb::types::SdkError<E> will implement Debug because it depends on what E is. Adding the trait bound as suggested above will fix the problem.

I thought the e parameter passed in the eprintln! macro
in both example is the same type:
SdkError<QueryError,Response>

It's working, thank you.

fn report_error<E : std::fmt::Debug>(e: aws_sdk_dynamodb::types::SdkError<E>) {
    eprintln!("DynamoDB error: {:?}", e);
}

Well, it doesn't work like that. In the inline snippet, the error type is concrete since there is no function boundary between it and the println!() call. If you pull the code out in a function, however, then you will have introduced a generic parameter, E.

Generic functions aren't dumb text-level substitutions, i.e., they are not like Rust macros, and in particular, they are not like C++ templates, either. The compiler typechecks the function body once, before it's instantiated with any concrete types, and it has to work at that point. In this way, Rust avoids surprise errors ("post-monomorphization errors"), whereby you meant your function to work with some types, but in reality it doesn't.

This in turn works by checking the abstract capabilities of each type parameter, so the compiler doesn't look at what concrete types it is called with (it couldn't possibly know that anyway, since the function might be called by downstream code). So if you don't tell the compiler that E must be Debug, then it can't assume that – it can only rely on capabilities (traits) you explicitly require the type parameter to have. So when you are calling the function, the compiler checks at the call site if these trait bounds are satisfied, and if so, it can be sure that the body doesn't need to be re-checked (and otherwise, you get an error at the call site, and not some mysterious error pointing inside a function down the line that you potentially didn't write).

This is much, much superior to C++ templates, which get re-checked with all expansions, and for which it is basically impossible to ensure that all intended types can be passed. On the contrary – Rust generics are impossible to write incorrectly, because they are type-checked right away.

3 Likes