Idiomatic error handling for resource cleaning

Hi,

exploring error handling in Rust vs other languages, I wonder what is the most idiomatic / clean way to handle these errors in rust (where dst is deleted if Copy or Close fails) ?

func CopyFile(src, dst string) error {
	r, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	defer r.Close()

	w, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	if _, err := io.Copy(w, r); err != nil {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	if err := w.Close(); err != nil {
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
}

(piece of code taken from Error Handling — Problem Overview)

What is the equivalent signature of this function in rust? Also, what is referred to by the defer keyword? Last, I presume that w stands for write and r for read?

defer is the equivalent of a function-wide c#-syle finally clause.

Being new to rust, I'm not sure how to express a function that may or may not return an error so my guess is

fn copy_file(src: &str, dst: &str) -> Result((), Error)

A defer statement defers the execution of a function until the surrounding function returns

Most idiomatic for file copy, is fs::copy :slight_smile:

For cleanup of files specifically, it's the tempfile crate (you can change a temp file to a permanent one on success).

In general, for cleanups, the idiomatic way in is to take advantage of the Drop trait. For example, file.close() doesn't exist and you never have to worry about calling it, because it's always closed when file goes out of scope. In Rust the destruction is deterministic (unlike finalization with languages with GC), so it's fine to use it to also manage external resources like files on disk.

If you really like Go's approach, there's scopeguard crate that emulates it, but that's usually a bit awkward and not idiomatic.

2 Likes

haha thank!

So if fs::copy didn't exists it could be something like (in pseudo Rust)

fn copy_file(src: &str, dst: &str) -> Result((), Error) {
  let src = fs::open(src)?;
  let dst = fs::create(dst)?;

  match fs::copy(dst, src) {
    Ok() => Ok(()),
    Err(err) => {
     fs::remove(dst);
     Err(err)
   }
 }

}

Or is there a better way using the Drop trait ?

No, it'd probably involve either a system call (Which is ugly if it's in the open) or copying the data:

fn copy_file(src: &str, dst: &str) -> Result<(), Box<std::error::Error>> {
    let src = fs::read(src)?;
    fs::write(dst, &src)?;
    Ok(())
}

An explanation:

let src = fs::read(src)?;

Will allocate the file in memory and store it in src as a Vec<u8>. The ? part at the end will automatically box the error into a generic error. This could be changed to std::io::Error instead, because these operations' errors are that error. The write will replace the contents (or create a new file) of a file with the &[u8] that it's passed.

1 Like

There's .map_err(|e| { fs::remove(); e })? to make it a one-liner. And if let Err(err) = fs::copy() {} if you don't care about the Ok case.

with a Drop it'd be:

fn copy_file(src_path: &Path, dst_path: &Path) -> io::Result<()> {
   let src = File::open(src_path)?;
   let dst = File::create(dst_path)?;
   let autodelete = Autodelete(dst_path);

   … do copying here …

   autodelete.dont_delete_actually();
}

where you'd implement your own struct Autodelete(PathBuf) with Drop impl that calls fs::remove() on the path unless you tell it otherwise.

It may seem more boilerplate than defer, but the difference is that you need this rarely. You implement Drop once per type ever, not once per use of that type.

From go's perspective, it behaves sort-of like:

func copy_file(src_path: &Path, dst_path: &Path) -> io::Result<()> {
   src := File::open(src_path)?;
   defer src.drop();
   dst := File::create(dst_path)?;
   defer dst.drop();
   autodelete := Autodelete(dst_path);
   defer autodelete.drop();
2 Likes

Interesting!
Thank you all!