Robustness of error handling and explicitness are two (of many) qualities of Rust for which I love the language. And I would like Rust to allow explicit "fallible drop".
Let's say you handle a resource that can return an error on closing, such a resource as a file or a stream of data from a remote connection that must be closed. If you want to do something in case of such a failure, e.g. notify a user, it would be amazing to be able to handle such errors.
One solution to this problem would be #[must_consume]
attribute. However, if you handle multiple resources, it gets complicated to manage.
Thanks to the reply of Alice Ryhl aka "alice" in Fallible Drop Method, I learned that...
...things such as
File
just ignore failures to close the file. It's not clear that there is much you can do about the failure, and triggering a panic in the destructor is prone to panics during panics, which immediately abort the process.
The solution that I see fit for this is introducing another rust-supported trait, std::ops::FallibleDrop
, that will be pretty much like std::ops::Drop
but offer fallible_drop
with a slightly different signature:
pub trait FallibleDrop<E: std::error::Error> {
// Required method
fn fallible_drop(&mut self) -> Result<(), E>;
}
In order to make a possibility of error on drop explicit, we can introduce #[fallible_drop]
attribute for let
statements.
fn example() -> Result<(), ConnectionErrorOnDrop> {
// Or #[fallible_drop(err_ty=ConnectionErrorOnDrop)]
#[fallible_drop]
let data_stream: DataStream = DataStream::connect()?;
// ...
// DataStream::fallible_drop()?;
}
If there are plans to stabilize std::ops::Try
trait, we can replace (F|f)allible
prefix with (T|t)ry
and it'll work just as fine.
As for defaults, I don't know. Both FallibleDrop
and Drop
have good reasons to be defaults. But we can specify it for a struct with a separate attribute:
#[default_drop(Drop)]
struct File {
//...
}
Additional thoughts
It should also affect the expression on the right-hand side.
Before desugaring:
#[fallible_drop]
let v = vec![DataStream::connect(HOST1)?,DataStream::connect(HOST2)?,DataStream::connect(HOST3?)];
After desugaring:
#[fallible_drop]
let v = {
let mut v = Vec::with_capacity(3);
v.push(DataStream::connect(HOST1)?);
// If the second connection fails, the first element in v has to be dropped too.
v.push(DataStream::connect(HOST2)?);
v.push(DataStream::connect(HOST3)?);
}
It should also probably be taken into consideration that proper support could be needed from 3rd party crates, notably tokio
:
Before desugaring:
#[tokio::main]
async fn main() -> Result<(), Error> {
#[fallible_drop]
let future = my_async_fn();
// future actually is taken by owned value and its type changes to Pin<...>
pin!(future);
(&mut future).await;
}
(I don't know what it desugars to)
And as a workaround, it could make sense to allow for #[fallible_drop]
on function/method definitions.