Drop and Resource Management

Destructors and Error Handling

Because Drop::drop(&mut self) has the return type () and is invoked implicitly by the compiler at scope exit, a destructor cannot return a Result<T, E> to its caller. When releasing a resource involves fallible operations—such as flushing buffered network data, syncing a file to disk, committing a database transaction, or joining a child process—relying solely on Drop hides errors or risks panicking during stack unwinding.

Why Drop::drop Returns ()

Every scope exit (}, return, break, ?, and stack unwinding) runs destructors automatically without a call site that could handle a returned Result. Therefore:

  1. Drop::drop must handle or log any internal cleanup failure silently without panicking.
  2. Types with fallible finalization should provide an explicit consuming close/finish method (fn close(self) -> Result<(), Error>) for the normal path, while keeping Drop as a best-effort fallback when the caller drops the value early due to an error or panic.
Explicit Consuming Finalizer + Fallback Drop Pattern:

  Caller invokes `writer.finish()?` (consumes `self` by value)
      |
      +-- Flushes & closes resource, returning `Result<(), Error>` to caller
      +-- Marks internal state as closed (or uses `ManuallyDrop`) so `Drop` is a no-op

  Caller exits early via `?` or panic without calling `.finish()`
      |
      +-- `Drop::drop(&mut self)` runs best-effort cleanup / rollback (ignoring or logging errors)

Implementing the Consuming close(self) -> Result Pattern

By storing an active flag or wrapping the inner handle in an Option, the consuming .close(mut self) method performs the fallible cleanup, sets the handle to None, and returns the Result to the caller. If .close() was never called, Drop::drop performs best-effort cleanup:

use std::io;

struct SpoolWriter {
    buffer: Vec<u8>,
    flushed: bool,
    simulate_disk_full: bool,
}

impl SpoolWriter {
    fn new(simulate_disk_full: bool) -> Self {
        Self {
            buffer: Vec::new(),
            flushed: false,
            simulate_disk_full,
        }
    }

    fn write(&mut self, data: &[u8]) {
        self.buffer.extend_from_slice(data);
    }

    fn flush_internal(&mut self) -> io::Result<()> {
        if self.simulate_disk_full {
            return Err(io::Error::new(io::ErrorKind::StorageFull, "disk full"));
        }
        println!("flushed {} bytes to disk", self.buffer.len());
        self.buffer.clear();
        self.flushed = true;
        Ok(())
    }

    /// Consumes the writer and returns any flush error to the caller.
    pub fn close(mut self) -> io::Result<()> {
        self.flush_internal()
    }
}

impl Drop for SpoolWriter {
    fn drop(&mut self) {
        if !self.flushed {
            if let Err(err) = self.flush_internal() {
                // Never panic inside Drop; log or ignore the error instead:
                println!("warning: best-effort flush in Drop failed: {}", err);
            }
        }
    }
}

fn main() {
    println!("=== 1. Explicit .close() succeeds ===");
    let mut ok_writer = SpoolWriter::new(false);
    ok_writer.write(b"hello");
    println!("close result: {:?}", ok_writer.close());

    println!("=== 2. Explicit .close() surfaces error ===");
    let mut err_writer = SpoolWriter::new(true);
    err_writer.write(b"important data");
    println!("close result: {:?}", err_writer.close());

    println!("=== 3. Implicit Drop logs warning without panicking ===");
    {
        let mut dropped_writer = SpoolWriter::new(true);
        dropped_writer.write(b"unclosed data");
    }
}
=== 1. Explicit .close() succeeds ===
flushed 5 bytes to disk
close result: Ok(())
=== 2. Explicit .close() surfaces error ===
close result: Err(Custom { kind: StorageFull, error: "disk full" })
=== 3. Implicit Drop logs warning without panicking ===
warning: best-effort flush in Drop failed: disk full

Rules for Error Handling Inside Drop::drop

Rule Rationale
Never call .unwrap() or .expect() in Drop::drop If Drop runs during a panic unwind, a second panic immediately aborts the process
Provide a consuming close(self) -> Result or finish(self) -> Result Gives callers a way to observe and recover from finalization/flush errors
Make Drop idempotent after close(self) Check an Option or boolean flag (or use ManuallyDrop) so Drop does not repeat work already completed by close(self)