公式の aws-sdk-rust を使うときの話です。まだ pre-release なので今後のバージョンでは話が変わるかもです。現在の最新である 0.10.1
での話です。
初期状態 もともとこんな感じでアプリケーションのエラーをまとめた enum を用意していました。これなら、main
でやっているように ?
が使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #[derive(thiserror::Error, Debug)] enum MyError { #[error("some application error has been occurred." )] AppHoge, #[error("aws api error has been occurred" )] Aws (#[from] aws_sdk_ec2::Error), } impl <T> From <aws_sdk_ec2::types::SdkError<T>> for MyError where aws_sdk_ec2::types::SdkError<T>: Into <aws_sdk_ec2::Error>, { fn from (e: aws_sdk_ec2::types::SdkError<T>) -> Self { MyError::Aws (e.into ()) } } #[tokio::main] async fn main () -> Result <(), MyError> { let config = aws_config::from_env ().load ().await ; let ec2 = aws_sdk_ec2::Client::new (&config); let ec2_result = ec2.describe_instances ().send ().await ?; Ok (()) }
問題発覚 EC2 以外の別サービスの呼び出しを追加した状態です。ここでは S3 の呼び出しを追加してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 #[tokio::main] async fn main () -> Result <(), MyError> { let config = aws_config::from_env ().load ().await ; let ec2 = aws_sdk_ec2::Client::new (&config); let s3 = aws_sdk_s3::Client::new (&config); let ec2_result = ec2.describe_instances ().send ().await ?; let s3_result = s3.list_buckets ().send ().await ?; Ok (()) }
これはコンパイルできなくて以下のエラーになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 error[E0277]: `?` couldn't convert the error to `aws_sdk_ec2::Error` --> src/main.rs:26:51 | 26 | let s3_result = s3.list_buckets().send().await?; | ^ the trait `From<SdkError<ListBucketsError>>` is not implemented for `aws_sdk_ec2::Error` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait = help: the following implementations were found: <aws_sdk_ec2::Error as From<SdkError<AcceptReservedInstancesExchangeQuoteError, R>>> <aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayMulticastDomainAssociationsError, R>>> <aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayPeeringAttachmentError, R>>> <aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayVpcAttachmentError, R>>> and 518 others = note: required because of the requirements on the impl of `Into<aws_sdk_ec2::Error>` for `SdkError<ListBucketsError>` = note: required because of the requirements on the impl of `From<SdkError<ListBucketsError>>` for `MyError` = note: required because of the requirements on the impl of `FromResidual<Result<Infallible, SdkError<ListBucketsError>>>` for `Result<(), MyError>`
aws_sdk_s3::types::SdkError<T>
は aws_sdk_ec2::Error
に変換できません。ここでまず思うのは「サービスをまたいだ共通のエラー構造体はあるのかな?」ということです。実は aws_sdk_s3::types::SdkError<T>
は aws_smithy_http::result::SdkError<T>
です。aws_sdk_ec2::types::SdkError<T>
も同様です。
そのため以下のようにエラーを表現することができるはできます。ただ、これをやると MyError
を利用する関数が全部 generic になって厳しいです。うまいことできる方法があったら知りたい……。
1 2 3 4 5 6 7 8 #[derive(thiserror::Error, Debug)] enum MyError <T> { #[error("some application error has been occurred." )] AppHoge, #[error("aws api error has been occurred" )] Aws (#[from] aws_sdk_ec2::types::SdkError<T>), }
Aws のエラーを2つに分けてみる 次に考えたのはこのパターンです。利用する AWS のサービスが増えるたびに追加が必要だけど、そこまで追加することはないからまぁいいか、という感覚です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #[derive(thiserror::Error, Debug)] enum MyError { #[error("some application error has been occurred." )] AppHoge, #[error("aws api error has been occurred" )] AwsEc2 (#[from] aws_sdk_ec2::Error), #[error("aws api error has been occurred" )] AwsS3 (#[from] aws_sdk_s3::Error), } impl <T> From <aws_sdk_ec2::types::SdkError<T>> for MyError where aws_sdk_ec2::types::SdkError<T>: Into <aws_sdk_ec2::Error>, { fn from (e: aws_sdk_ec2::types::SdkError<T>) -> Self { MyError::Aws (e.into ()) } } impl <T> From <aws_sdk_s3::types::SdkError<T>> for MyError where aws_sdk_s3::types::SdkError<T>: Into <aws_sdk_s3::Error>, { fn from (e: aws_sdk_s3::types::SdkError<T>) -> Self { MyError::Aws (e.into ()) } }
ところがこれはダメです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 error[E0119]: conflicting implementations of trait `std::convert::From<aws_sdk_ec2::types::SdkError<_>>` for type `MyError` --> src/main.rs:22:1 | 13 | / impl<T> From<aws_sdk_ec2::types::SdkError<T>> for MyError 14 | | where 15 | | aws_sdk_ec2::types::SdkError<T>: Into<aws_sdk_ec2::Error>, 16 | | { ... | 19 | | } 20 | | } | |_- first implementation here 21 | 22 | / impl<T> From<aws_sdk_s3::types::SdkError<T>> for MyError 23 | | where 24 | | aws_sdk_s3::types::SdkError<T>: Into<aws_sdk_s3::Error>, 25 | | { ... | 28 | | } 29 | | } | |_^ conflicting implementation for `MyError`
これがどうコンフリクトしているのかいまだに理解できていないです。aws_sdk_ec2::types::SdkError<T>
と aws_sdk_s3::types::SdkError<T>
が同じものを指していることまではわかりますが、aws_sdk_ec2::Error
と aws_sdk_s3::Error
は別物なのでコンフリクトしなそうに思えてしまっています。
この状態から長いことガチャガチャいじってみたけどうまく解決しなかったので次の作戦に移行しました。
Box にぶち込む これがひとまずの最終形です。AWS 系エラーは1つにまとめましたが、内部の型を残すのをあきらめました。これなら AWS のサービスが増えても何も変える必要がないです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[derive(thiserror::Error, Debug)] enum MyError { #[error("some application error has been occurred." )] AppHoge, #[error("aws api error has been occurred" )] Aws (Box <dyn std::error::Error + Send + Sync + 'static >), } impl <T> From <aws_sdk_ec2::types::SdkError<T>> for MyError where T: std::error::Error + Send + Sync + 'static , { fn from (e: aws_sdk_ec2::types::SdkError<T>) -> Self { MyError::Aws (Box ::new (e)) } }
もしもエラーから具体的なものが必要になったら以下のようにダウンキャストもできるのでよしとしておきましょう。
1 2 3 4 5 if let MyError ::Aws (e) = error { if let Some (e) = e.downcast_ref::<aws_sdk_ec2::types::SdkError<DescribeInstancesError>>() { } }