Skip to content

Error Handling

gershnik edited this page Apr 3, 2022 · 6 revisions

General

Argum can operate in 3 modes selected at compile time:

  • Using exceptions (this is the default). In this mode parsing errors produce exceptions.
  • Using expected values, with exceptions enabled. In this mode parsing errors are reported via Expected<T> return values. This class similar to proposed std::expected or boost::outcome. Trying to access a result.value() when it contains an error throws an equivalent exceptions. This mode is enabled via ARGUM_USE_EXPECTED macro.
  • With exceptions disabled. In this mode expected values used as above but trying to access a result.value() when it contains an error calls std::terminate. This mode can be manually enabled via ARGUM_NO_THROW macro. On Clang, GCC and MSVC Argum automatically detects if exceptions are disabled during compilation and switches to this mode.

Note that these modes only affect handling of parsing errors. Logic errors such as passing incorrect parameters to configure parser always assert in debug and std::terminate in non-debug builds.

Also note that, unless you are building with exceptions disabled, various standard library facilities may throw exceptions (e.g. std::bad_alloc) that will propagate out of Parser methods.

Error/Exception Safety

Whether you use exceptions or expected values Argum's parser provides the following safety guarantees:

  • Non-const methods of parser provide basic safety guarantee. If an exception is thrown/error returned the only safe thing to do is to destroy the parser object. (There is no clear or reset method to bring it to a known state).
  • Const methods, notably parse, provide strong safety guarantee. If an exception is thrown the state of the parser is unchanged

Errors

The same classes are thrown as exceptions in exception mode and as errors in expected values one. All of the derive from either ParsingException or WParsingException base classes, which themselves derive from std::exception.

The base classes provide message() member function that returns an std::string_view or std::wstring_view respectively containing the error message.

If you ever need to differentiate between different error types you have multiple options:

  • catch-ing them in exception throwing mode
  • dynamic_cast-ing them if you have RTTI enabled
  • Using provided as<T>() member method that performs a safe cast without RTTI. For example
    auto result = parse(...);
    if (auto error = result.error()) {
        //let's treat a specific error differently
        if (auto ambiguousOptionError = error ->as<Parser::AmbiguousOption>()) {
            //here ambiguousOptionError is Parser::AmbiguousOption *
        }
    }

The currently defined exception classes are:

[W]ParsingException - base class

  • [W]ResponseFileReader::Exception - issues opening or reading response file. Members:
    std::filesystem::path filename;
    std::error_code       error;
  • [W]Parser::UnrecognizedOption - an unrecognized option. Members:
    std::[w]string option;
  • [W]Parser::AmbiguousOption - an option is ambiguous (e.g. --fo when --foo and --foobar are defined). Members:
    std::[w]string option;
    std::vector<std::[w]string> possibilities;
  • [W]Parser::MissingOptionArgument - required option argument is not provided. Members:
    std::[w]string option;
  • [W]Parser::ExtraOptionArgument - option that has no arguments is given one (e.g. --no-arg-option=arg). Members:
    std::[w]string option;
  • [W]Parser::ExtraPositional - an unexpected positional argument has been provided. Members:
    std::[w]string value;
  • [W]Parser::ValidationError - validation of an option or argument or overall constraints has failed. This class has no extra members.

Expected Values

Since there is currently no standard expected value type, Argum uses its own. It is called BasicExpected<Char, T> where Char argument specifies the character type for the errors it can hold and T is the expected type. For convenience, as usual, there are two typedefs: Expected<T> and WExpected<T> that use char and wchar_t respectively. In what follows we will use Expected as a shorthand for either of these.

An instance of Expected<T> contains either a T value or an error - a [shared] pointer to a basic class ParsingException. These can be obtained and manipulated via its methods. Note that it is possible to have Expected<void>. In fact this is exactly what regular parse() method returns. In this case the stored "value" is nothing but the relevant methods returning it are still there (for generic code) - they just return void.

Here is how to use Expected:

  • Canonical check for error and use expected value otherwise:

    auto result = someFunction(...);
    if (auto error = result.error()) { //.error() returns std::shared_ptr<ParsingException> 
       //Error path, you can:
       //propagate Expected
       return error; 
       //or log and return
       cout << error->message(); 
       return;
       ...
       //etc.
    } else {
       //Success path. Here we can use the expected value if desired
       //operator* doesn't perform any checks. It is safe to use here because the error check in the if
       auto value = *result;
    }
  • Boolean checks

    auto result = someFunction(...);
    if (result) {
       //result contains value
    }
    if (!result) {
       //result contain error
    }

    Note that unless you don't care about the actual error, it is more efficient to call error() and compare the result to nullptr than perform a boolean check and call error() anyway.

  • Accessing value

    auto result = someFunction(...);
    
    //Safe:
    // - will throw contained error if the value is not present (if exceptions are enabled) or abort (if not)
    // - not efficient if you already know the value is there
    auto value = result.value(); 
    
    //Unsafe:
    // - does not check if the value is there so the behavior is undefined if it isn't
    // - efficient if you know the value is there
    auto value = *result;
    
    //Unsafe member access
    //Operator -> is just like * but returns a pointer. Also unsafe without a prior check.
    //Only defined if T is not void
    result->memberFunction();
  • Constructing

    //default construct default constructs the value
    Expected<std::string> expected;
    assert(*expected == "");
    
    //from value
    Expected<int> expected = 7; 
    Expected<int> expected(7); //or {7}
    
    //calling value constructor in-place
    Expected<std::string> expected(5, 'c');
    assert(*expected == "ccccc");
    
    //from shared pointer to error
    catch(ParsingException & ex) {
       //clone() method returns a shared pointer to cloned exception value
       Expected<int> expected(std::move(ex).clone());
    }
    
    //with error constructed in place
    //this constructs expected holding Parser::ValidationError with a message "invalid argument"
    //Failure<type of error to store> marks this as an "error" constructor
    Expected<int> expected(Failure<Parser::ValidationError>, "invalid argument");
    
  • Conversions
    Any Expected<T> can be converted into Expected<AnotherT> if T is convertible to AnotherT. If an expected stores an error then the error is carried over. If it stores a value then the value is converted.

    Expected<int> expectedInt = someFunction();
    Expected<long> expectedLong = expectedInt;

    As a convenience any Expected<T> can be converted to Expected<void>. This simply discards the value if no error or carries over the error.

    Expected<void> foo() {
        Expected<std::string> result = someFunc();
        if (!result)
            return result; //this works due to conversion to Expected<void>
        //use *result
    }