ExamplesBy LevelBy TopicLearning Paths
1024 Intermediate

1024-file-errors — File Operation Errors

Functional Programming

Tutorial

The Problem

File I/O is one of the most common sources of recoverable errors in production software. Files may not exist, permissions may be wrong, disks may be full, or paths may be invalid. Every language with a type system faces the question of how to represent these errors at the type level.

Rust's std::io::Error unifies all I/O errors into a single type with an ErrorKind discriminant for runtime classification. This enables generic I/O code while still allowing precise error handling where needed.

🎯 Learning Outcomes

  • • Use std::fs functions and handle io::Error return types
  • • Classify errors by io::ErrorKind (NotFound, PermissionDenied, etc.)
  • • Convert io::Error to application-specific error types
  • • Understand how io::Error carries an OS error code alongside its kind
  • • Chain file operations with ? in a larger pipeline
  • Code Example

    #![allow(clippy::all)]
    // 1024: File Operation Errors
    // std::io::Error kinds and handling
    
    use std::fs;
    use std::io::{self, Write};
    use std::path::Path;
    
    // Approach 1: Basic file operations with io::Error
    fn read_file(path: &str) -> Result<String, io::Error> {
        fs::read_to_string(path)
    }
    
    fn write_file(path: &str, content: &str) -> Result<(), io::Error> {
        fs::write(path, content)
    }
    
    // Approach 2: Classifying io::Error by kind
    fn classify_io_error(err: &io::Error) -> &'static str {
        match err.kind() {
            io::ErrorKind::NotFound => "file not found",
            io::ErrorKind::PermissionDenied => "permission denied",
            io::ErrorKind::AlreadyExists => "already exists",
            io::ErrorKind::InvalidInput => "invalid input",
            io::ErrorKind::TimedOut => "timed out",
            io::ErrorKind::Interrupted => "interrupted",
            io::ErrorKind::WouldBlock => "would block",
            _ => "other IO error",
        }
    }
    
    // Approach 3: Converting io::Error to app-specific error
    #[derive(Debug)]
    enum FileError {
        NotFound(String),
        PermissionDenied(String),
        Other(String),
    }
    
    impl std::fmt::Display for FileError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                FileError::NotFound(p) => write!(f, "file not found: {}", p),
                FileError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
                FileError::Other(msg) => write!(f, "file error: {}", msg),
            }
        }
    }
    
    fn read_file_typed(path: &str) -> Result<String, FileError> {
        fs::read_to_string(path).map_err(|e| match e.kind() {
            io::ErrorKind::NotFound => FileError::NotFound(path.into()),
            io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.into()),
            _ => FileError::Other(e.to_string()),
        })
    }
    
    // Safe file operation with existence check
    fn read_if_exists(path: &str) -> Result<Option<String>, io::Error> {
        if Path::new(path).exists() {
            fs::read_to_string(path).map(Some)
        } else {
            Ok(None)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_read_nonexistent() {
            let err = read_file("/nonexistent_file_12345").unwrap_err();
            assert_eq!(err.kind(), io::ErrorKind::NotFound);
        }
    
        #[test]
        fn test_classify_not_found() {
            let err = io::Error::new(io::ErrorKind::NotFound, "test");
            assert_eq!(classify_io_error(&err), "file not found");
        }
    
        #[test]
        fn test_classify_permission() {
            let err = io::Error::new(io::ErrorKind::PermissionDenied, "test");
            assert_eq!(classify_io_error(&err), "permission denied");
        }
    
        #[test]
        fn test_write_read_roundtrip() {
            let tmp = "/tmp/rust_test_1024.txt";
            write_file(tmp, "hello rust").unwrap();
            let content = read_file(tmp).unwrap();
            assert_eq!(content, "hello rust");
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_typed_error() {
            let err = read_file_typed("/nonexistent_12345").unwrap_err();
            assert!(matches!(err, FileError::NotFound(_)));
            assert!(err.to_string().contains("not found"));
        }
    
        #[test]
        fn test_read_if_exists() {
            let result = read_if_exists("/nonexistent_12345").unwrap();
            assert!(result.is_none());
    
            let tmp = "/tmp/rust_test_1024b.txt";
            fs::write(tmp, "exists").unwrap();
            let result = read_if_exists(tmp).unwrap();
            assert_eq!(result, Some("exists".to_string()));
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_io_error_display() {
            let err = io::Error::new(io::ErrorKind::NotFound, "missing.txt");
            assert_eq!(err.to_string(), "missing.txt");
        }
    
        #[test]
        fn test_error_kind_matching() {
            // io::ErrorKind is an enum — exhaustive matching available
            let err = fs::read_to_string("/no_such_file_xyz").unwrap_err();
            match err.kind() {
                io::ErrorKind::NotFound => {} // expected
                other => panic!("unexpected error kind: {:?}", other),
            }
        }
    }

    Key Differences

  • Exception vs Result: OCaml's Unix file functions raise exceptions by default; Rust's std::fs functions always return Result.
  • Error classification: Both use OS error codes internally; Rust exposes them via ErrorKind enum, OCaml via Unix.error variant.
  • Error composition: Rust's ? propagates io::Error through call stacks uniformly; OCaml's try/with scoping is more explicit.
  • Cross-platform: Rust's io::ErrorKind abstracts OS differences; OCaml's Unix.error is more Unix-specific.
  • OCaml Approach

    OCaml's Unix module raises exceptions for file errors:

    let read_file path =
      try
        let ic = open_in path in
        let content = In_channel.input_all ic in
        close_in ic;
        Ok content
      with
      | Sys_error msg -> Error msg
      | Unix.Unix_error (code, fn_name, arg) ->
        Error (Unix.error_message code)
    

    The Unix.error type is a variant with constructors like ENOENT, EACCES, etc., analogous to io::ErrorKind.

    Full Source

    #![allow(clippy::all)]
    // 1024: File Operation Errors
    // std::io::Error kinds and handling
    
    use std::fs;
    use std::io::{self, Write};
    use std::path::Path;
    
    // Approach 1: Basic file operations with io::Error
    fn read_file(path: &str) -> Result<String, io::Error> {
        fs::read_to_string(path)
    }
    
    fn write_file(path: &str, content: &str) -> Result<(), io::Error> {
        fs::write(path, content)
    }
    
    // Approach 2: Classifying io::Error by kind
    fn classify_io_error(err: &io::Error) -> &'static str {
        match err.kind() {
            io::ErrorKind::NotFound => "file not found",
            io::ErrorKind::PermissionDenied => "permission denied",
            io::ErrorKind::AlreadyExists => "already exists",
            io::ErrorKind::InvalidInput => "invalid input",
            io::ErrorKind::TimedOut => "timed out",
            io::ErrorKind::Interrupted => "interrupted",
            io::ErrorKind::WouldBlock => "would block",
            _ => "other IO error",
        }
    }
    
    // Approach 3: Converting io::Error to app-specific error
    #[derive(Debug)]
    enum FileError {
        NotFound(String),
        PermissionDenied(String),
        Other(String),
    }
    
    impl std::fmt::Display for FileError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                FileError::NotFound(p) => write!(f, "file not found: {}", p),
                FileError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
                FileError::Other(msg) => write!(f, "file error: {}", msg),
            }
        }
    }
    
    fn read_file_typed(path: &str) -> Result<String, FileError> {
        fs::read_to_string(path).map_err(|e| match e.kind() {
            io::ErrorKind::NotFound => FileError::NotFound(path.into()),
            io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.into()),
            _ => FileError::Other(e.to_string()),
        })
    }
    
    // Safe file operation with existence check
    fn read_if_exists(path: &str) -> Result<Option<String>, io::Error> {
        if Path::new(path).exists() {
            fs::read_to_string(path).map(Some)
        } else {
            Ok(None)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_read_nonexistent() {
            let err = read_file("/nonexistent_file_12345").unwrap_err();
            assert_eq!(err.kind(), io::ErrorKind::NotFound);
        }
    
        #[test]
        fn test_classify_not_found() {
            let err = io::Error::new(io::ErrorKind::NotFound, "test");
            assert_eq!(classify_io_error(&err), "file not found");
        }
    
        #[test]
        fn test_classify_permission() {
            let err = io::Error::new(io::ErrorKind::PermissionDenied, "test");
            assert_eq!(classify_io_error(&err), "permission denied");
        }
    
        #[test]
        fn test_write_read_roundtrip() {
            let tmp = "/tmp/rust_test_1024.txt";
            write_file(tmp, "hello rust").unwrap();
            let content = read_file(tmp).unwrap();
            assert_eq!(content, "hello rust");
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_typed_error() {
            let err = read_file_typed("/nonexistent_12345").unwrap_err();
            assert!(matches!(err, FileError::NotFound(_)));
            assert!(err.to_string().contains("not found"));
        }
    
        #[test]
        fn test_read_if_exists() {
            let result = read_if_exists("/nonexistent_12345").unwrap();
            assert!(result.is_none());
    
            let tmp = "/tmp/rust_test_1024b.txt";
            fs::write(tmp, "exists").unwrap();
            let result = read_if_exists(tmp).unwrap();
            assert_eq!(result, Some("exists".to_string()));
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_io_error_display() {
            let err = io::Error::new(io::ErrorKind::NotFound, "missing.txt");
            assert_eq!(err.to_string(), "missing.txt");
        }
    
        #[test]
        fn test_error_kind_matching() {
            // io::ErrorKind is an enum — exhaustive matching available
            let err = fs::read_to_string("/no_such_file_xyz").unwrap_err();
            match err.kind() {
                io::ErrorKind::NotFound => {} // expected
                other => panic!("unexpected error kind: {:?}", other),
            }
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_read_nonexistent() {
            let err = read_file("/nonexistent_file_12345").unwrap_err();
            assert_eq!(err.kind(), io::ErrorKind::NotFound);
        }
    
        #[test]
        fn test_classify_not_found() {
            let err = io::Error::new(io::ErrorKind::NotFound, "test");
            assert_eq!(classify_io_error(&err), "file not found");
        }
    
        #[test]
        fn test_classify_permission() {
            let err = io::Error::new(io::ErrorKind::PermissionDenied, "test");
            assert_eq!(classify_io_error(&err), "permission denied");
        }
    
        #[test]
        fn test_write_read_roundtrip() {
            let tmp = "/tmp/rust_test_1024.txt";
            write_file(tmp, "hello rust").unwrap();
            let content = read_file(tmp).unwrap();
            assert_eq!(content, "hello rust");
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_typed_error() {
            let err = read_file_typed("/nonexistent_12345").unwrap_err();
            assert!(matches!(err, FileError::NotFound(_)));
            assert!(err.to_string().contains("not found"));
        }
    
        #[test]
        fn test_read_if_exists() {
            let result = read_if_exists("/nonexistent_12345").unwrap();
            assert!(result.is_none());
    
            let tmp = "/tmp/rust_test_1024b.txt";
            fs::write(tmp, "exists").unwrap();
            let result = read_if_exists(tmp).unwrap();
            assert_eq!(result, Some("exists".to_string()));
            fs::remove_file(tmp).unwrap();
        }
    
        #[test]
        fn test_io_error_display() {
            let err = io::Error::new(io::ErrorKind::NotFound, "missing.txt");
            assert_eq!(err.to_string(), "missing.txt");
        }
    
        #[test]
        fn test_error_kind_matching() {
            // io::ErrorKind is an enum — exhaustive matching available
            let err = fs::read_to_string("/no_such_file_xyz").unwrap_err();
            match err.kind() {
                io::ErrorKind::NotFound => {} // expected
                other => panic!("unexpected error kind: {:?}", other),
            }
        }
    }

    Deep Comparison

    File Operation Errors — Comparison

    Core Insight

    File operations always fail — the question is how richly you can classify and handle those failures.

    OCaml Approach

  • Sys_error of string — one exception for all I/O failures
  • • Must parse the string to distinguish NotFound vs PermissionDenied
  • open_in/open_out raise exceptions — need try/with
  • • Cleanup via Fun.protect ~finally
  • Rust Approach

  • std::io::Error with ErrorKind enum — structured classification
  • fs::read_to_string returns Result<String, io::Error>
  • • Match on err.kind() for specific handling
  • • RAII handles cleanup (files close when dropped)
  • Comparison Table

    AspectOCamlRust
    Error typeSys_error of stringio::Error with ErrorKind
    ClassificationParse error stringMatch ErrorKind enum
    File readopen_in + really_input_stringfs::read_to_string
    CleanupFun.protect ~finallyRAII / Drop trait
    Custom errorsWrap in variantmap_err to app error
    Error infoString message onlyKind + message + OS code

    Exercises

  • Write a read_or_create(path: &str, default: &str) -> Result<String, io::Error> function that reads a file if it exists, or creates it with the default content if it does not.
  • Implement a safe_copy(src: &str, dst: &str) -> Result<u64, FileError> function that copies a file, converting all io::Errors to FileError.
  • Write a function that lists all .txt files in a directory using fs::read_dir, collecting errors and file paths separately.
  • Open Source Repos