Skip to main content

dynamodb_facade/
error.rs

1use aws_sdk_dynamodb::types::WriteRequest;
2use thiserror::Error;
3
4/// A specialized [`Result`](core::result::Result) type for this crate.
5///
6/// All fallible operations in `dynamodb-facade` return this type.
7///
8/// # Examples
9///
10/// ```
11/// use dynamodb_facade::{Error, Result};
12///
13/// fn validate_role(role: &str) -> Result<()> {
14///     if role.is_empty() {
15///         return Err(Error::custom("role must not be empty"));
16///     }
17///     Ok(())
18/// }
19///
20/// assert!(validate_role("student").is_ok());
21/// assert!(validate_role("").is_err());
22/// ```
23pub type Result<T> = core::result::Result<T, Error>;
24
25/// The error type for all `dynamodb-facade` operations.
26///
27/// Wraps the various failure modes that can occur when interacting with
28/// DynamoDB: SDK-level transport and service errors, serialization failures,
29/// and application-defined errors.
30///
31/// # Variants
32///
33/// - [`Error::DynamoDB`] — an error originating from the AWS SDK, such as a
34///   `ConditionalCheckFailedException`, a throttling error, or a network
35///   failure. Use [`Error::as_dynamodb_error`] to inspect the underlying
36///   [`aws_sdk_dynamodb::Error`].
37/// - [`Error::Serde`] — a (de)serialization failure produced by
38///   [`serde_dynamo`] when converting between Rust types and DynamoDB items.
39/// - [`Error::Other`] — any other boxed [`core::error::Error`]. Useful for
40///   wrapping domain errors via [`Error::other`].
41/// - [`Error::FailedBatchWrite`] — a batch write that could not complete
42///   after all retry attempts. Contains the unprocessed [`WriteRequest`]s.
43/// - [`Error::Custom`] — a plain string error message. Useful for quick
44///   ad-hoc errors via [`Error::custom`].
45///
46/// # Examples
47///
48/// Matching on error variants:
49///
50/// ```
51/// use dynamodb_facade::Error;
52///
53/// fn handle(err: Error) {
54///     match err {
55///         Error::DynamoDB(_)          => eprintln!("AWS SDK error"),
56///         Error::Serde(_)             => eprintln!("serialization error"),
57///         Error::FailedBatchWrite(r)  => eprintln!("{} items unprocessed", r.len()),
58///         Error::Other(_)             => eprintln!("other error"),
59///         Error::Custom(msg)          => eprintln!("custom error: {msg}"),
60///     }
61/// }
62/// ```
63#[derive(Debug, Error)]
64pub enum Error {
65    /// An error returned by the AWS DynamoDB SDK.
66    ///
67    /// This variant is produced automatically via the [`From`] impls for
68    /// [`aws_sdk_dynamodb::error::SdkError`] and [`aws_sdk_dynamodb::Error`].
69    /// Use [`Error::as_dynamodb_error`] to borrow the inner error for
70    /// pattern-matching on specific service errors such as
71    /// `ConditionalCheckFailedException`.
72    #[error(transparent)]
73    DynamoDB(Box<aws_sdk_dynamodb::Error>),
74
75    /// A (de)serialization error from [`serde_dynamo`].
76    ///
77    /// Produced when converting a Rust struct to or from a DynamoDB item map
78    /// fails — for example, when a required attribute is missing or has an
79    /// unexpected type.
80    #[error(transparent)]
81    Serde(#[from] serde_dynamo::Error),
82
83    /// A batch write that did not complete after all retry attempts.
84    ///
85    /// Returned by [`dynamodb_batch_write`](crate::dynamodb_batch_write) when
86    /// some [`WriteRequest`]s remain unprocessed after the maximum number of
87    /// retries. The contained vector holds the requests that were never
88    /// acknowledged by DynamoDB, allowing the caller to inspect or retry them.
89    #[error("BatchWriteItem failure: {len} items", len = .0.len())]
90    FailedBatchWrite(Vec<WriteRequest>),
91
92    /// Any other boxed error.
93    ///
94    /// Use [`Error::other`] to wrap an arbitrary [`core::error::Error`] value
95    /// into this variant.
96    #[error(transparent)]
97    Other(#[from] Box<dyn core::error::Error + Send>),
98
99    /// A plain string error message.
100    ///
101    /// Use [`Error::custom`] to construct this variant.
102    #[error("Custom Error: {0}")]
103    Custom(String),
104}
105
106impl Error {
107    /// Creates an [`Error::Custom`] from any value that converts into a
108    /// [`String`].
109    ///
110    /// This is a convenience constructor for quick ad-hoc errors without
111    /// needing to define a dedicated error type.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use dynamodb_facade::Error;
117    ///
118    /// let err = Error::custom("enrollment limit reached");
119    /// assert!(matches!(err, Error::Custom(_)));
120    /// assert_eq!(err.to_string(), "Custom Error: enrollment limit reached");
121    /// ```
122    pub fn custom(message: impl Into<String>) -> Self {
123        Self::Custom(message.into())
124    }
125
126    /// Creates an [`Error::Other`] by boxing any [`core::error::Error`] value.
127    ///
128    /// Use this to wrap domain-specific or standard-library errors when
129    /// implementing fallible methods from this crate — for example, a
130    /// [`FromStr`](std::str::FromStr) parse error inside a manual
131    /// [`DynamoDBItem::try_from_item`](crate::DynamoDBItem::try_from_item)
132    /// implementation.
133    ///
134    /// # Examples
135    ///
136    /// Wrapping a [`ParseIntError`](std::num::ParseIntError) when deserializing
137    /// a DynamoDB string attribute into a numeric field:
138    ///
139    /// ```
140    /// use dynamodb_facade::Error;
141    ///
142    /// fn parse_credits(raw: &str) -> dynamodb_facade::Result<u32> {
143    ///     raw.parse::<u32>().map_err(Error::other)
144    /// }
145    ///
146    /// assert!(parse_credits("42").is_ok());
147    /// assert!(matches!(parse_credits("not-a-number"), Err(Error::Other(_))));
148    /// ```
149    pub fn other(error: impl core::error::Error + Send + Sync + 'static) -> Self {
150        Self::Other(Box::new(error))
151    }
152
153    /// Returns a reference to the inner [`aws_sdk_dynamodb::Error`] if this
154    /// error is the [`Error::DynamoDB`] variant, or [`None`] otherwise.
155    ///
156    /// Use this to inspect or pattern-match on specific DynamoDB service
157    /// errors (e.g. `ConditionalCheckFailedException`, `ResourceNotFoundException`)
158    /// without unwrapping the full error chain.
159    ///
160    /// # Examples
161    ///
162    /// Distinguishing a "not found" condition failure from other errors when
163    /// deleting an enrollment that must already exist:
164    ///
165    /// ```no_run
166    /// # use dynamodb_facade::{DynamoDBItemOp, DynamoDBError, KeyId};
167    /// # use dynamodb_facade::test_fixtures::*;
168    /// # async fn example(
169    /// #     client: dynamodb_facade::Client,
170    /// #     user_id: &str,
171    /// #     course_id: &str,
172    /// # ) -> Result<Enrollment, String> {
173    /// match Enrollment::delete_by_id(client, KeyId::pk(user_id).sk(course_id))
174    ///     .exists()
175    ///     .await
176    /// {
177    ///     Ok(enrollment) => Ok(enrollment.expect("exists guard guarantees a return value")),
178    ///     Err(e) if matches!(
179    ///         e.as_dynamodb_error(),
180    ///         Some(DynamoDBError::ConditionalCheckFailedException(_))
181    ///     ) => Err(format!("enrollment for user {user_id} / course {course_id} not found")),
182    ///     Err(e) => Err(format!("unexpected error: {e}")),
183    /// }
184    /// # }
185    /// ```
186    pub fn as_dynamodb_error(&self) -> Option<&aws_sdk_dynamodb::Error> {
187        match self {
188            Self::DynamoDB(e) => Some(e),
189            _ => None,
190        }
191    }
192}
193
194/// Converts an [`aws_sdk_dynamodb::error::SdkError`] into [`Error::DynamoDB`].
195///
196/// This impl is provided for all `SdkError<T, R>` where the SDK can convert
197/// the operation-specific error into the generic [`aws_sdk_dynamodb::Error`].
198/// It allows the `?` operator to be used directly on SDK call results.
199impl<T, R> From<aws_sdk_dynamodb::error::SdkError<T, R>> for Error
200where
201    aws_sdk_dynamodb::Error: From<aws_sdk_dynamodb::error::SdkError<T, R>>,
202{
203    fn from(value: aws_sdk_dynamodb::error::SdkError<T, R>) -> Self {
204        Self::DynamoDB(Box::new(value.into()))
205    }
206}
207
208/// Converts an [`aws_sdk_dynamodb::Error`] into [`Error::DynamoDB`].
209///
210/// Boxes the SDK error and wraps it in the [`Error::DynamoDB`] variant.
211impl From<aws_sdk_dynamodb::Error> for Error {
212    fn from(value: aws_sdk_dynamodb::Error) -> Self {
213        Self::DynamoDB(Box::new(value))
214    }
215}