When using glob::glob, how do you properly handle errors vs non-matching paths?

The glob crate's glob() function returns an iterator that yields Result<PathBuf, GlobError>, where errors indicate problems reading directory contents rather than paths that don't match. Understanding this distinction is crucial for robust file system traversal code.

Basic Glob Usage

use glob::glob;
 
fn basic_usage() {
    for entry in glob("src/**/*.rs").unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => eprintln!("Error: {:?}", e),
        }
    }
}

The iterator yields Result values where errors represent I/O problems, not missing files.

What Constitutes an Error

use glob::{glob, GlobError, GlobResult};
 
fn understanding_errors() {
    // Errors occur when:
    // 1. Insufficient permissions to read a directory
    // 2. Broken symbolic links
    // 3. Directory was deleted during iteration
    // 4. Other I/O errors
    
    for result in glob("/root/**/*").unwrap() {
        match result {
            Ok(path) => println!("Path: {:?}", path),
            Err(GlobError { path, error }) => {
                // path: The path that caused the error
                // error: The underlying io::Error
                eprintln!("Failed to access {:?}: {}", path, error);
            }
        }
    }
}

A non-matching pattern simply yields an empty iterator, not an error.

Non-Matching Patterns

use glob::glob;
 
fn non_matching_patterns() {
    // Pattern with no matches yields empty iterator
    let pattern = "nonexistent_directory/*.rs";
    
    // glob() itself succeeds even if nothing matches
    let count = glob(pattern).unwrap().count();
    
    println!("Found {} matches", count);  // "Found 0 matches"
    
    // This is NOT an error - just no matches
    for entry in glob("nonexistent/*.txt").unwrap() {
        // This body never executes
        println!("Found: {:?}", entry);
    }
}

Zero matches is a valid result, distinct from an error.

Pattern Syntax Errors vs Runtime Errors

use glob::{glob, PatternError};
 
fn pattern_errors() {
    // Invalid pattern syntax returns an error immediately
    let result = glob("[invalid");
    
    match result {
        Err(PatternError { pos, msg }) => {
            // Pattern syntax error
            eprintln!("Invalid pattern at position {}: {}", pos, msg);
        }
        Ok(iter) => {
            // Pattern is valid, iterate over matches
            for entry in iter {
                // Handle results
            }
        }
    }
    
    // Common pattern syntax errors:
    // - Unmatched brackets: "[abc"
    // - Invalid escape: "\\"
    // - Empty alternatives: "{,a}"
}

Pattern errors occur at pattern compilation time; I/O errors occur during iteration.

Proper Error Handling Pattern

use glob::{glob, GlobError};
use std::path::PathBuf;
 
fn proper_handling() -> Result<Vec<PathBuf>, Vec<(PathBuf, std::io::Error)>> {
    let mut matches = Vec::new();
    let mut errors = Vec::new();
    
    for entry in glob("src/**/*.rs").map_err(|e| vec![(PathBuf::new(), e.into())])? {
        match entry {
            Ok(path) => matches.push(path),
            Err(GlobError { path, error }) => {
                errors.push((path, error));
            }
        }
    }
    
    if errors.is_empty() {
        Ok(matches)
    } else {
        Err(errors)
    }
}
 
fn use_proper_handling() {
    match proper_handling() {
        Ok(paths) => {
            println!("Found {} files", paths.len());
            for path in paths {
                println!("  {:?}", path);
            }
        }
        Err(errors) => {
            eprintln!("Encountered {} errors:", errors.len());
            for (path, error) in errors {
                eprintln!("  {:?}: {}", path, error);
            }
        }
    }
}

Collect both matches and errors separately for comprehensive handling.

Handling Permission Errors

use glob::{glob, GlobError};
use std::io;
 
fn permission_handling() {
    for entry in glob("/var/log/**/*.log").unwrap() {
        match entry {
            Ok(path) => {
                println!("Found log: {:?}", path);
                // Process the file
            }
            Err(GlobError { path, error }) => {
                match error.kind() {
                    io::ErrorKind::PermissionDenied => {
                        eprintln!("Permission denied: {:?}", path);
                        // Optionally skip and continue
                    }
                    io::ErrorKind::NotFound => {
                        // Path was deleted during iteration
                        eprintln!("Disappeared: {:?}", path);
                    }
                    _ => {
                        eprintln!("Error accessing {:?}: {}", path, error);
                    }
                }
            }
        }
    }
}

Different error kinds may warrant different handling strategies.

Continuing Despite Errors

use glob::{glob, GlobError};
 
fn continue_on_errors() {
    let mut found = Vec::new();
    let mut errors = Vec::new();
    
    for entry in glob("/etc/**/*.conf").unwrap() {
        match entry {
            Ok(path) => found.push(path),
            Err(e) => errors.push(e),
        }
        // Continue iterating even after errors
    }
    
    println!("Found {} files", found.len());
    if !errors.is_empty() {
        eprintln!("{} errors occurred", errors.len());
    }
    
    // Process found files
    for path in found {
        // ...
    }
}

Errors don't stop iteration; you can continue collecting matches.

The GlobResult Type Alias

use glob::GlobResult;
 
fn using_globresult() {
    // GlobResult is a type alias for Result<PathBuf, GlobError>
    fn process_entries(entries: impl Iterator<Item = GlobResult>) {
        for result in entries {
            match result {
                Ok(path) => println!("Match: {:?}", path),
                Err(e) => eprintln!("Error: {}", e),
            }
        }
    }
    
    process_entries(glob("*.rs").unwrap());
}

GlobResult is the type you'll work with from the iterator.

Filtering Results

use glob::glob;
 
fn filter_results() {
    // Only collect successful matches
    let matches: Vec<_> = glob("src/**/*.rs")
        .unwrap()
        .filter_map(Result::ok)
        .collect();
    
    println!("Found {} Rust files", matches.len());
    
    // Or use filter for only errors
    let errors: Vec<_> = glob("/root/**/*")
        .unwrap()
        .filter_map(Result::err)
        .collect();
    
    println!("{} errors occurred", errors.len());
}

filter_map(Result::ok) is common when you want to ignore errors.

Complete Error Context

use glob::{glob, GlobError};
 
fn detailed_error_handling() {
    for entry in glob("/var/**/*.log").unwrap() {
        match entry {
            Ok(path) => {
                // Successfully matched path
                println!("Matched: {:?}", path);
            }
            Err(GlobError { path, error }) => {
                // Path exists but couldn't be accessed
                eprintln!("Error on path {:?}", path);
                eprintln!("  Error kind: {:?}", error.kind());
                eprintln!("  Error message: {}", error);
                
                // Get more context
                if let Some(inner) = error.get_ref() {
                    eprintln!("  Inner error: {:?}", inner);
                }
            }
        }
    }
}

GlobError provides both the problematic path and the underlying error.

Patterns with No Matches

use glob::glob;
 
fn no_matches() {
    // These patterns might match nothing
    let patterns = [
        "nonexistent/**/*.rs",
        "src/*.cpp",      // No C++ files in src
        "*.nonexistent",  // No such extension
    ];
    
    for pattern in patterns {
        let matches: Vec<_> = glob(pattern)
            .unwrap()
            .filter_map(Result::ok)
            .collect();
        
        if matches.is_empty() {
            println!("Pattern '{}' matched nothing", pattern);
        } else {
            println!("Pattern '{}' matched {} files", pattern, matches.len());
        }
    }
    
    // This is different from a pattern error:
    match glob("[") {
        Ok(_) => println!("Pattern is valid"),
        Err(e) => eprintln!("Invalid pattern: {}", e),
    }
}

Empty results and invalid patterns are distinct cases.

Symlink Handling

use glob::{glob, GlobError};
 
fn symlink_handling() {
    // glob follows symlinks by default
    for entry in glob("/usr/local/**/*.so").unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(GlobError { path, error }) => {
                // Broken symlinks cause errors
                eprintln!("Broken link or inaccessible: {:?}", path);
            }
        }
    }
    
    // Use glob_with to control symlink behavior
    use glob::MatchOptions;
    
    let options = MatchOptions {
        follow_links: false,  // Don't follow symlinks
        ..Default::default()
    };
    
    for entry in glob_with("/usr/local/**/*.so", options).unwrap() {
        // Symlinks are returned as paths but not followed
        match entry {
            Ok(path) => println!("Path (possibly symlink): {:?}", path),
            Err(e) => eprintln!("Error: {:?}", e),
        }
    }
}

Broken symlinks cause GlobError, not missing matches.

MatchOptions for Control

use glob::{glob_with, MatchOptions};
 
fn match_options() {
    let options = MatchOptions {
        case_sensitive: false,     // Case-insensitive matching
        require_literal_separator: false,  // * matches /
        require_literal_leading_dot: false,  // * matches .files
        follow_links: true,        // Follow symlinks
    };
    
    for entry in glob_with("**/*.RS", options).unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => eprintln!("Error: {}", e),
        }
    }
}

Options control matching behavior and error conditions.

Combining Multiple Patterns

use glob::glob;
 
fn multiple_patterns() {
    let patterns = ["src/**/*.rs", "tests/**/*.rs", "examples/**/*.rs"];
    
    let mut all_rust_files = Vec::new();
    let mut all_errors = Vec::new();
    
    for pattern in patterns {
        for entry in glob(pattern).unwrap() {
            match entry {
                Ok(path) => all_rust_files.push(path),
                Err(e) => all_errors.push(e),
            }
        }
    }
    
    println!("Found {} Rust files", all_rust_files.len());
    if !all_errors.is_empty() {
        eprintln!("{} errors occurred", all_errors.len());
    }
}

Each pattern has its own potential for errors.

Distinguishing Error Types

use glob::{glob, GlobError};
use std::io;
 
fn classify_errors() {
    for entry in glob("/system/**/*.dat").unwrap() {
        if let Err(GlobError { path, error }) = entry {
            match error.kind() {
                io::ErrorKind::PermissionDenied => {
                    // Skip silently or log
                    eprintln!("Skipping (no access): {:?}", path);
                }
                io::ErrorKind::NotFound => {
                    // Race condition - file deleted during glob
                    eprintln!("Disappeared: {:?}", path);
                }
                io::ErrorKind::Other => {
                    // Could be broken symlink
                    if error.to_string().contains("symlink") {
                        eprintln!("Broken symlink: {:?}", path);
                    } else {
                        eprintln!("Other error: {:?} - {}", path, error);
                    }
                }
                _ => {
                    eprintln!("Unexpected error: {:?} - {}", path, error);
                }
            }
        }
    }
}

Different errors may need different responses.

Practical Example: Robust File Search

use glob::{glob, GlobError};
use std::path::PathBuf;
use std::io;
 
#[derive(Debug)]
struct SearchResult {
    matches: Vec<PathBuf>,
    permission_denied: Vec<PathBuf>,
    not_found: Vec<PathBuf>,
    other_errors: Vec<(PathBuf, io::Error)>,
}
 
fn search_files(pattern: &str) -> SearchResult {
    let mut result = SearchResult {
        matches: Vec::new(),
        permission_denied: Vec::new(),
        not_found: Vec::new(),
        other_errors: Vec::new(),
    };
    
    match glob(pattern) {
        Ok(iter) => {
            for entry in iter {
                match entry {
                    Ok(path) => result.matches.push(path),
                    Err(GlobError { path, error }) => {
                        match error.kind() {
                            io::ErrorKind::PermissionDenied => {
                                result.permission_denied.push(path);
                            }
                            io::ErrorKind::NotFound => {
                                result.not_found.push(path);
                            }
                            _ => {
                                result.other_errors.push((path, error));
                            }
                        }
                    }
                }
            }
        }
        Err(e) => {
            // Pattern syntax error
            eprintln!("Invalid pattern: {}", e);
        }
    }
    
    result
}
 
fn use_search() {
    let result = search_files("/var/log/**/*.log");
    
    println!("Found {} matches", result.matches.len());
    
    if !result.permission_denied.is_empty() {
        eprintln!("{} directories inaccessible", result.permission_denied.len());
    }
    
    if !result.other_errors.is_empty() {
        eprintln!("{} other errors", result.other_errors.len());
    }
}

A complete solution categorizes all outcomes.

Checking if Pattern is Valid

use glob::{glob, Pattern};
 
fn validate_pattern(pattern: &str) -> bool {
    Pattern::new(pattern).is_ok()
}
 
fn check_patterns() {
    let patterns = [
        "src/**/*.rs",      // Valid
        "*.txt",            // Valid
        "[",                // Invalid - unmatched bracket
        "**/*.rs",          // Valid
    ];
    
    for pattern in patterns {
        if validate_pattern(pattern) {
            println!("Valid pattern: {}", pattern);
            
            // Can safely call glob()
            let matches: Vec<_> = glob(pattern)
                .unwrap()
                .filter_map(Result::ok)
                .collect();
            
            println!("  {} matches", matches.len());
        } else {
            eprintln!("Invalid pattern: {}", pattern);
        }
    }
}

Validate patterns before using them to avoid syntax errors.

Synthesis

When using glob::glob, proper error handling distinguishes three cases:

  1. Pattern syntax errors: Returned immediately from glob() as Err(PatternError). Example: invalid bracket syntax like "[".

  2. Runtime I/O errors: Yielded as Err(GlobError) during iteration. These include:

    • Permission denied
    • Broken symlinks
    • Files deleted during iteration
    • Other filesystem errors
  3. No matches: Not an error. The iterator simply yields zero items. An empty result is valid.

use glob::{glob, GlobError};
 
fn robust_glob(pattern: &str) {
    // Handle pattern error
    let iter = match glob(pattern) {
        Ok(iter) => iter,
        Err(e) => {
            eprintln!("Invalid pattern: {}", e);
            return;
        }
    };
    
    // Handle iteration results
    for entry in iter {
        match entry {
            Ok(path) => {
                // Successfully matched path
                println!("Match: {:?}", path);
            }
            Err(GlobError { path, error }) => {
                // Runtime error accessing path
                eprintln!("Error at {:?}: {}", path, error);
            }
        }
    }
    
    // Empty iterator = no matches (not an error)
}

Best practices:

  • Validate patterns if they come from user input
  • Handle both Ok and Err from the iterator
  • Use filter_map(Result::ok) only when you want to silently ignore errors
  • Classify errors by io::ErrorKind for appropriate responses
  • Remember that an empty result means no matches, not an error