Documents
Exit Codes And Error Handling
Exit Codes And Error Handling
Type
Topic
Status
Published
Created
Mar 1, 2026
Updated
Mar 1, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Exit Codes And Error Handling#

IMPORTANT NOTE: The background context provided for this article references the "Gold Digger" project with database-specific exit codes (EXIT_NO_ROWS, EXIT_DB_AUTH_ERROR, EXIT_QUERY_ERROR) and SQL credential redaction. However, the actual codebase is libmagic-rs (a file type identification tool), which has a different exit code taxonomy and no database/SQL functionality. This article documents the actual implementation found in the libmagic-rs repository.

Overview#

Exit codes and error handling in libmagic-rs implement a Unix-convention exit code taxonomy with five distinct non-zero codes distinguishing configuration errors, file access issues, magic file problems, and resource limit violations. The system uses Rust's thiserror crate for structured error classification through a hierarchy of LibmagicError, ParseError, and EvaluationError types.

The error-to-exit-code mapping operates through the handle_error function in src/main.rs, which delegates to specialized handlers (handle_io_error, handle_parse_error_new, handle_evaluation_error_new, handle_timeout_error) that examine error subtypes and return appropriate exit codes. Exit codes are returned as numeric literals (0-5) rather than named constants.

The implementation provides automation-friendly error semantics through a strict mode flag: standard mode continues processing all files and exits 0 even on errors, while strict mode captures the first error and returns its exit code after processing completes. This design prioritizes batch processing utility while maintaining precise error signaling when needed.

Exit Code Taxonomy#

Code Definitions#

The rmagic CLI tool uses six distinct exit codes:

Exit CodeMeaningTrigger Conditions
0SuccessAll files processed without errors
1General errorEvaluationError or ConfigError
2Misuse of shell commandIoError(InvalidInput)
3File not found or access deniedIoError(NotFound/PermissionDenied) or FileError
4Magic file not found or invalidParseError
5Evaluation timeout or resource limits exceededTimeout error

The taxonomy follows BSD-style exit code conventions: codes 0-2 align with standard Unix semantics (success, general error, usage error), while codes 3-5 provide application-specific error classification.

Design Rationale#

The exit code design prioritizes user-facing error classification over internal error granularity. File access errors (exit 3), argument validation errors (exit 2), and magic file problems (exit 4) receive distinct codes because they represent different corrective actions for users. Internal errors like EvaluationError variants (buffer overrun, recursion limit exceeded, invalid string encoding) all map to exit code 1 because they signal "file cannot be evaluated" regardless of the internal cause.

This decision reflects the tool's role as a Unix filter utility: automation scripts need to distinguish "file doesn't exist" (retry with different path) from "magic file corrupt" (fix configuration) from "evaluation timed out" (adjust limits), but do not need to distinguish between different evaluation failure modes.

Error Classification System#

Error Type Hierarchy#

The error taxonomy uses three primary error types defined with Rust's thiserror crate:

LibmagicError (top-level enum):

  • ParseError(ParseError) - Magic file parsing failures → exit 4
  • EvaluationError(EvaluationError) - File evaluation failures → exit 1
  • IoError(std::io::Error) - I/O operations → exit 2 or 3
  • Timeout { timeout_ms: u64 } - Evaluation timeout → exit 5
  • ConfigError { reason: String } - Configuration problems → exit 1
  • FileError(String) - File access problems → exit 3

ParseError (8 variants):

  • InvalidSyntax - Malformed magic file syntax
  • UnsupportedFeature - Magic file uses unsupported features
  • InvalidOffset / InvalidType / InvalidOperator / InvalidValue - Invalid magic rule components
  • UnsupportedFormat - Binary .mgc files not supported
  • IoError - I/O failure during magic file reading

EvaluationError (9 variants):

  • BufferOverrun - Read beyond file boundaries
  • InvalidOffset - Offset calculation failed
  • UnsupportedType - Type not implemented
  • RecursionLimitExceeded - Indirect rule depth exceeded
  • StringLengthExceeded - String longer than limit
  • InvalidStringEncoding - String decoding failed
  • Timeout - Evaluation took too long
  • TypeReadError - Failed to read typed data
  • InternalError - Bug in evaluator (should not occur)

Error Mapping Implementation#

The handle_error function performs top-level error classification through pattern matching on LibmagicError variants:

fn handle_error(error: LibmagicError) -> i32 {
    match error {
        LibmagicError::IoError(ref io_err) => handle_io_error(io_err),
        LibmagicError::ParseError(ref parse_err) => handle_parse_error_new(parse_err),
        LibmagicError::EvaluationError(ref eval_err) => handle_evaluation_error_new(eval_err),
        LibmagicError::Timeout { timeout_ms } => handle_timeout_error(timeout_ms),
        LibmagicError::ConfigError { ref reason } => {
            eprintln!("Configuration error: {reason}");
            1
        }
        LibmagicError::FileError(ref msg) => {
            eprintln!("File error: {msg}");
            3
        }
    }
}

Delegated error handlers inspect error subtypes:

handle_io_error examines std::io::ErrorKind:

  • NotFound → exit 3 ("File not found")
  • PermissionDenied → exit 3 ("Permission denied")
  • InvalidInput → exit 2 ("Invalid input")
  • Other → exit 3 ("File access failed")

handle_parse_error_new treats all ParseError variants uniformly → exit 4

handle_evaluation_error_new treats all EvaluationError variants uniformly → exit 1

handle_timeout_error handles timeout errors → exit 5

Each handler prints a descriptive error message to stderr before returning its exit code, ensuring users receive actionable diagnostics even in non-strict mode.

Error Propagation Pattern#

thiserror-Based Error Types#

Unlike ad-hoc error handling, libmagic-rs uses structured error types built with the thiserror crate. Each error variant includes the #[error("...")] attribute for automatic Display implementation and the #[from] attribute for automatic conversion:

#[derive(Debug, thiserror::Error)]
pub enum LibmagicError {
    #[error("Parse error: {0}")]
    ParseError(#[from] ParseError),

    #[error("Evaluation error: {0}")]
    EvaluationError(#[from] EvaluationError),

    #[error("I/O error: {0}")]
    IoError(#[from] std::io::Error),
    // ...
}

The #[from] attribute enables automatic error type conversion when using the ? operator: a function returning Result<T, LibmagicError> can propagate a std::io::Error directly via ?, which automatically wraps it in LibmagicError::IoError.

Result Type Usage#

All fallible functions return Result<T, LibmagicError> and use the ? operator for error propagation. Example from load_magic_database:

fn load_magic_database(args: &Args) -> Result<MagicDatabase, LibmagicError> {
    let config = args.to_evaluation_config();

    if args.use_builtin {
        return MagicDatabase::with_builtin_rules_and_config(config);
    }

    let magic_file_path = args.get_magic_file_path();

    if !magic_file_path.exists() {
        return Err(LibmagicError::ParseError(
            libmagic_rs::ParseError::invalid_syntax(0, format!("Magic file not found..."))
        ));
    }

    validate_magic_file(&magic_file_path)?; // Propagates error via ?

    MagicDatabase::load_from_file_with_config(&magic_file_path, config)
}

The ? operator propagates errors up the call stack until they reach run_analysis, where per-file error handling logic determines whether to accumulate errors or return immediately.

Batch Processing Error Semantics#

Standard vs Strict Mode#

The run_analysis function implements two error handling modes controlled by the --strict flag:

Standard Mode (default):

  • Errors for individual files are printed to stderr
  • Processing continues for all remaining files
  • Function returns Ok(()) regardless of errors
  • Exit code is 0

Strict Mode (--strict flag):

  • First error is captured in first_error variable
  • Processing continues for all files (errors still printed)
  • After all files processed, returns first Err encountered
  • Results in non-zero exit code via handle_error

Implementation pattern:

let mut first_error: Option<LibmagicError> = None;

for file_or_stdin in &args.files {
    match process_file(file_or_stdin, &db, args) {
        Ok(()) => {}
        Err(e) => {
            eprintln!("Error processing {}: {}", file_or_stdin.filename(), e);
            if first_error.is_none() {
                first_error = Some(e);
            }
        }
    }
}

if let Some(error) = first_error {
    if args.strict {
        return Err(error);
    }
}

Ok(())

This design reflects a deliberate trade-off: graceful degradation (process what you can) in standard mode vs strict failure signaling (report first error to caller) in strict mode. Both modes complete processing of all files to maximize utility of each invocation.

Automation Implications#

The dual-mode error handling affects automation differently:

Monitoring scripts should use --strict to detect any failures across a batch of files. Exit code 0 guarantees all files succeeded; non-zero exit indicates at least one file failed (check stderr for details).

Best-effort processing scripts should use standard mode to maximize output even when some files fail. Exit code 0 does not guarantee all files succeeded; must parse stderr to identify individual failures.

The main function unconditionally calls process::exit(exit_code), ensuring the exit code is always propagated to the shell.

Usage Examples#

Exit Code Testing#

Test specific error conditions by examining the exit code:

# Success case
rmagic /etc/passwd
echo $? # 0

# File not found (exit 3)
rmagic /nonexistent/file
echo $? # 3

# Permission denied (exit 3)
touch /tmp/noaccess
chmod 000 /tmp/noaccess
rmagic /tmp/noaccess
echo $? # 3

# Invalid magic file (exit 4)
echo "invalid syntax" > /tmp/bad.magic
rmagic -m /tmp/bad.magic /etc/passwd
echo $? # 4

# Timeout (exit 5, requires crafted input)
rmagic --timeout 1 <file-that-triggers-recursion>
echo $? # 5

Automation Scripts#

Check for specific error classes in shell scripts:

#!/bin/bash

rmagic "$FILE"
EXIT_CODE=$?

case $EXIT_CODE in
    0)
        echo "Success"
        ;;
    3)
        echo "File access error - check path and permissions"
        exit 1
        ;;
    4)
        echo "Magic file configuration error - check --magic-file"
        exit 1
        ;;
    5)
        echo "Timeout - consider increasing --timeout"
        exit 1
        ;;
    *)
        echo "General error - see stderr"
        exit 1
        ;;
esac

Strict Mode for CI Pipelines#

Use --strict in CI/CD to fail fast on any error:

# Process all files, exit non-zero if any fail
rmagic --strict *.bin

# Equivalent to:
if rmagic --strict *.bin; then
    echo "All files processed successfully"
else
    echo "At least one file failed (exit code $?)"
    exit 1
fi

Relevant Code Files#

File PathPurposeKey Components
src/main.rsCLI entry point and error handlinghandle_error, handle_io_error, handle_parse_error_new, handle_evaluation_error_new, handle_timeout_error, run_analysis, main
src/error.rsError type definitionsLibmagicError, ParseError, EvaluationError
Cargo.tomlDependency declarationsthiserror dependency
  • Magic File Format: The ParseError taxonomy reflects supported and unsupported magic file syntax
  • Evaluation Configuration: Timeout errors (exit 5) are controlled by EvaluationConfig settings
  • CLI Argument Parsing: Invalid arguments trigger InvalidInput I/O errors (exit 2)
  • Strict Mode: The --strict flag affects error propagation in batch processing

Architectural Context#

The exit code system represents a durable architectural decision about error signaling for automation. Key design principles:

  1. User-facing classification over internal granularity: File access errors, magic file errors, and timeouts get distinct codes because they require different remediation. Internal evaluation failures share exit code 1.

  2. Unix convention adherence: Exit codes 0-2 follow BSD conventions, making the tool a well-behaved Unix citizen.

  3. Graceful degradation by default: Standard mode processes all files and exits 0 even on errors, maximizing utility for exploratory use. Strict mode enables failure detection for monitoring.

  4. Numeric literals over constants: Exit codes are returned as raw integers (0-5) rather than named constants, reducing indirection in error handlers.

  5. Structured error types over string messages: thiserror-based error types enable compile-time exhaustiveness checking when adding new error variants.

These decisions balance automation-friendliness (predictable exit codes), user-friendliness (descriptive stderr messages), and developer-friendliness (type-safe error handling) without compromising any dimension.

Exit Codes And Error Handling | Dosu