Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get from exceptions to user error messages

When we learn in school user data validation is most often blatantly ignored. I taught myself to validate data with the exception mechanism, but when I try to print useful user messages it feel very clumsy to do that from the exceptions. The more I try the more I feel like exceptions are meant for "inside the code", and they should not "escape" to the end user.

Let's take a simple example. The user needs to enter a command. The format can be: cmd i j or cmd with cmd a predefined set of commands and i and j numbers. We then execute that command.

What I have, from bottom to up is this:

// utility function, converts string to enum
str_to_enum(string str, map<enum, string> s) -> enum
   // may throw std::invalid_argument{"cannot convert " + str + " to enum"}

parse_line(string line)
   tokens = split(line)
   
   Cmd cmd = tokens[0]; // calls str_to_enum, lets the exception pass by 

   if (tokens.empty())
      throw std::invalid_argument{"empty string as command"s};

   if (...)
      throw std::invalid_argument{cmd.spelling() + " does not take any argument."};

   if (...)
      throw std::invalid_argument{cmd.spelling() + " takes exactly 2 arguments."};

  str_to_int(tokens[1])
  str_to_int(tokens[2]) // throws std::out_of_range


main_loop() {
    get_line(line);

    try {
        Full_cmd full_cmd = line; // calls parse_line, may throw

        if (...)
            throw std::invalid_argument{"Coordinates out of range"};

        try {
            execute_cmd(full_cmd); // may throw      
        }
        catch (std::exception& e) {
             cerr << "error executing command:" << endl;
             cerr << e.what() << endl << endl;
        }
    }
    catch (const std::exception& e) {
        cerr << "Invalid command '"s << line << "': " << endl;
        cerr << e.what() << endl;
    }
}

I think the above logic can be easily followed.

Now, if I want to just display the cryptic "Invalid command" and "Error executing command" then all would be easy. But I want meaningful messages, like the ones I tried above. One problem is that e.what doesn't feel the proper vesel for this. It contains succinct, often technical details. Another problem is that for instance the error "cannot convert <input_string> to enum" reaches the user. While he gets the real string he input, the enum part is implementation detail and criptic for him.

What I see are 2 solutions:

  1. Use std::exception errors.

    • catch errors at almost any step and rethrow with a reworked message. E.g. from "Cannot convert to enum" to "Command not found".
    • At the topmost level catch std::exception and basically print e.what()
  2. create exception classes for each type of error (e.g. invalid_command, invalid_no_args etc..

  • If necessary add more information (other data members) to it in order to have the complete context of the error
  • once an exception is thrown, it bubbles up to the topmost level
  • Then at the top-most level, catch each custom exception type and print accordingly.

I obviously went with the first approach. The big downside are the imperfect messages the user gets.

For the second one I feel there is a disproportionate effort vs gain. There would be a lot of boring redundant work just to create the custom exception classes. I tried it once and gave up after the 7'th almost identical class each fully equipped custom data members and constructors and what methods (that set and use those members)

Also, couldn't help but feel I'm reinventing the wheel.

My question is: are exceptions solely a good tool to convey error messages to the end user? If so what is the right way of doing it? If not, how can it be done?

like image 309
bolov Avatar asked Oct 03 '16 20:10

bolov


2 Answers

The first approach is not the right approach, for two reasons:

  1. As you have witnessed, it represents a tremendous amount of work.

  2. It presumes that programmers may possibly manage to code into an exception object an error message that could potentially be meaningful to a user. They can't; It wouldn't. If not for any other reason, then at least because statistically speaking, the language in which the programmers will write the message is unlikely to be a language understood by the user. (And I mean this primarily from a linguistic standpoint, though the "level of technicality" standpoint is also worth considering.)

The second approach is in the right direction, but with some modifications:

  1. Drastically reduce the number of exceptions that your system may throw by preemptively checking for mistakes and presenting error messages like "I will not allow you to do this" instead of messages like "what you did was bad, and it failed".

  2. Drastically reduce the number of exception classes that you have to define at each level of your system by making each exception simply stand for "failed to [do whatever this level was trying to do] because such-and-such exception was thrown by a level below". By storing a reference of the "causal" exception into the new exception, you save yourself from having to write lots of new exceptions at each level, with lots of member variables etc.

  3. Realize that human-readable error messages inside exceptions are completely useless: never write such a message, and never show such a message to the user. The message of an exception is the class name of the exception. If you have RTTI, (Run-Time-Type-Information,) use it. Otherwise, just make sure that each exception object knows the name of its own class. This is for your eyes only, and when you see it, you know unambiguously which exception it is.

  4. At the top level of your system, only display error messages to the user about exceptions that you can test. Yes, this means that you have to be able to set up your system so that the exception will actually be thrown, so that your testing code can make sure that the exception was caught and the right error message was issued.

  5. For exceptions that you cannot test, do not even bother showing a message. Just display a general-purpose "something went wrong" error, (along with a nice humorous image perhaps,) and append as much information as you can to your application's log, for your forensic analysis later.

like image 77
Mike Nakis Avatar answered Nov 15 '22 03:11

Mike Nakis


TL;DR

My question is: are exceptions solely a good tool to convey error messages to the end user? If so what is the right way of doing it? If not, how can it be done?

Yes, exceptions are solely meant to convey error/exeptional conditions to the client.

They are not meant to introduce any program control flow in conjunction with catch() or final statements.


The big downside are the imperfect messages the user gets.

You have many options to provide useful messages using the std::runtime_error exception.

like image 44
πάντα ῥεῖ Avatar answered Nov 15 '22 04:11

πάντα ῥεῖ