You may have seen the following error message on a Unix system:

No such file or directory

This comes from the Unix standard library for the C language. It's the textual representation of errno value ENOENT. errno is a global variable provided by the C standard library variable for the error code specifying the cause of failure for a system call.

In Unix, when a program asks the operating system kernel to do something, it does this by using a system call. This might be something like "open a file for reading". For this blog post, it's "run this program".

If the system call fails, it returns an error indicator. For most system calls the return value is an integer and -1 tells the caller the system call failed. The indicator value does not tell the caller why the system call failed. Perhaps the program doesn't exist? Perhaps it exists, but the user does not have permission to run it?

The reason for the failure is stored in the errno global variable, by the C standard library. Other languages provide ways of getting the value. The errno value is also an integer, and there are C macros defined in the errno.h include file for the various possible values, and the linked Wikipedia page has examples. The integer value can be translated into a static textual message using the C standard library function strerror(3).

A single integer can't describe the cause of the problem with much detail. Unix programs are meant to know what system call they used and what arguments they gave to it, and use this to construct a useful error message. For example, if the program was opening a file, it should use the name of file it tried to open combined with the text from strerror to produce an error message like:

Failed to run /does/not/exist: No such file or directory

The C standard library does not have tools to make such error messages easily. Thus, most Unix programs just print out the text returned by strerror without additional information.

If you think this is a convoluted, sub-par way of dealing with errors, and that it's insane and stupid that this has situation has lasted largely unchanged since the 1970s, I'm not going to stop you.

Rust is not better by default. If you use the Rust std::process::Command data type to run a program, without taking care, you'll end up with the same error message:

No such file or directory (os error 2)

There is the additional information of "os error code 2", which is the errno value, but that's of no use to a user.

The above message comes from this Rust snippet:

match Command::new("/does/not/exist").output() {
    Ok(output) => {
        panic!("this was not meant to succeed");
    }
    Err(e) => eprintln!("{e}"),
}

As a user I'd like to know at least what did the program I run try to do and using what file. To achieve this in Rust, we need to inspect the returned error in more detail. It's doable with the Rust standard library, but it does require a little effort.

Below is an example of how to do it. It uses thiserror to define an error type specific for this, but no other crates apart from the standard library. It's not perfect, and the error messages can certainly be improved, but it's a start.

// This is an example of handing errors when executing another
// program. We use the `std::process::Command` type in the standard
// library to do this, but add a little wrapper to handle the various
// errors that can go wrong. As I only use Unix, this is a little Unix
// specific.
//
// I wrote this because I was annoyed by programs saying "No such file
// or directory" when trying to run a program that doesn't exist, and
// I wanted to make sure Rust programs can do better.

use std::{
    // We need this trait to handle underlying errors, when writing
    // out error messages.
    error::Error,

    // The program name is technically an OsString. We avoid the
    // simplifying assumption that it is UTF8, because there is no
    // guarantee that the name of a program is UTF8, in Unix.
    ffi::OsString,

    // We need these to actually invoke programs.
    process::{Command, Output, Stdio},
};

// On Unix, we need this to find out which signal terminated the
// program.
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;

// This main program exercises the spawn function we have below.
fn main() {
    // Create a `Command` to run the first command line argument, but
    // don't start running it yet.
    let mut cmd = Command::new(std::env::args().nth(1).unwrap());

    // Optionally, redirect the command's standard I/O. Here we close
    // stdin, and capture stdout and stderr. Any other setup can be
    // done at this point as well. This is a separate variable from
    // `cmd`, because the methods we use return a mutable reference to
    // `cmd`, instead of transferring ownership.
    let cmd2 = cmd
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    // Actually run the command and wait for it to terminate. The
    // `spawn` function starts running the program, if that is
    // possible.
    match spawn(cmd2) {
        // All went well.
        Ok(output) => {
            let stdout = String::from_utf8_lossy(&output.stdout);
            let stderr = String::from_utf8_lossy(&output.stderr);
            println!("captured stdout: {stdout:?}");
            println!("captured stderr: {stderr:?}");
            println!("All good");
        }

        // Program ran, but it failed. Report that and also its
        // captured stdout and stderr, assuming we set that up above.
        Err(CommmandError::CommandFailed {
            program,
            exit_code,
            output,
        }) => {
            eprintln!("ERROR: {program:?} failed: {exit_code}");
            let stdout = String::from_utf8_lossy(&output.stdout);
            let stderr = String::from_utf8_lossy(&output.stderr);
            println!("captured stdout: {stdout:?}");
            println!("captured stderr: {stderr:?}");
        }

        // Report any other program, including its underlying problem,
        // if any.
        Err(e) => {
            eprintln!("ERROR: failed to run program: {e}");
            let mut e = e.source();
            while e.is_some() {
                let underlying = e.unwrap();
                eprintln!("caused by: {underlying}");
                e = underlying.source();
            }
            std::process::exit(42);
        }
    }
}

// Given a `Command` that has been set up, spawn the child process,
// and wait for it to finish, then handle the result.
fn spawn(cmd: &mut Command) -> Result<Output, CommmandError> {
    let child = match cmd.spawn() {
        // Child process started running OK.
        Ok(child) => child,

        // Program does not exist.
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            return Err(CommmandError::NoSuchCommand(cmd.get_program().into()))
        }

        // We lack permission to run program.
        Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
            return Err(CommmandError::NoPermmission(cmd.get_program().into()))
        }

        // Other problem prevented the program from starting.
        Err(err) => return Err(CommmandError::Other(cmd.get_program().into(), err)),
    };

    // Wait for child to terminate, and capture its output, in case
    // that was set up.
    match child.wait_with_output() {
        // Child terminated, but it may have failed.
        Ok(output) => {
            // Did the child terminate due to a signal?
            #[cfg(unix)]
            if let Some(signal) = output.status.signal() {
                return Err(CommmandError::KilledBySignal(
                    cmd.get_program().into(),
                    signal,
                ));
            }

            // Did the child terminate with a non-zero exit code?
            if let Some(code) = output.status.code() {
                if code != 0 {
                    return Err(CommmandError::CommandFailed {
                        program: cmd.get_program().into(),
                        exit_code: code,
                        output,
                    });
                }
            }

            // At this point we know the child terminated, because we
            // used `wait_with_output`. We also know that it didn't
            // fail, because it didn't get killed by a signal, and it
            // didn't have a non-zero exit code.
            assert!(output.status.success());

            Ok(output)
        }

        // Something unexpected went wrong.
        Err(err) => Err(CommmandError::Other(cmd.get_program().into(), err)),
    }
}

// All the errors that can go wrong when running a program. We embed
// the name of the program in the error variants, so it doesn't get
// lost. This makes for better error messages in my opinion.
#[derive(Debug, thiserror::Error)]
enum CommmandError {
    // The program doesn't exist. Or, possibly, the program specifies
    // an interpreter or shared library that does not exist. The
    // operating system doesn't tell us which.
    #[error("command does not exist: {0:?}")]
    NoSuchCommand(OsString),

    // The program exists, but we lack the permission to run it. On
    // Unix, this means the program file lacks the x bit for us.
    #[error("no permission to run command: {0:?}")]
    NoPermmission(OsString),

    // The program ran, but terminated with a non-zero exit code. Note
    // that this error variant includes any captured stdout and stderr
    // output, if the `Command` was created to capture them.
    #[error("command failed: {program:?}: exit code {exit_code}")]
    CommandFailed {
        program: OsString,
        exit_code: i32,
        output: Output,
    },

    // The program ran, but was terminated by a signal.
    #[cfg(unix)]
    #[error("command was terminated by signal {1}: {0:?}")]
    KilledBySignal(OsString, i32),

    // There was some other error. There can be any number of errors,
    // and over time they can vary. We can't know everything, so we
    // have a catchall error variant to handle the unknowns.
    #[error("unknown error while running command: {0:?}")]
    Other(OsString, #[source] std::io::Error),
}

I've not tried to encapsulate this into a library. Maybe some of the myriad existing Rust libraries to run programs already does this.