Search

7.15 — Detecting and handling errors

In lesson 7.14 -- Common semantic errors in C++, we covered many types of common C++ semantic errors that new C++ programmers run into with the language. If an error is the result of a misused language feature or logic error, the error can simply be corrected.

But most errors in a program don’t occur as the result of inadvertently misusing language features -- rather, most errors occur due to faulty assumptions made by the programmer and/or a lack of proper error detection/handling.

For example, in a function designed to look up a grade for a student, you might have assumed:

  • The student being looked up will exist.
  • All student names will be unique.
  • The class uses letter grading (instead of pass/fail).

What if any of these assumptions aren’t true? If the programmer didn’t anticipate these cases, the program will likely malfunction or crash when such cases arise (usually at some point in the future, well after the function has been written).

There are three key places where assumption errors typically occur:

  • When a function returns, the programmer may have assumed the called function was successful when it was not.
  • When program receives input (either from the user, or a file), the programmer may have assumed the input was in the correct format and semantically valid when it was not.
  • When a function has been called, the programmer may have assumed the parameters would be semantically valid when they were not.

Many new programmers write code and then only test the happy path: only the cases where there are no errors. But you should also be planning for and testing your sad paths, where things can and will go wrong. In lesson 3.10 -- Finding issues before they become problems, we defined defensive programming as the practice of trying to anticipate all of the ways software can be misused, either by end-users, or by developers (either the programmer themselves, or others). Once you’ve anticipated (or discovered) some misuse, the next thing to do is handle it.

In this lesson, we’ll talk about error handling strategies (what to do when things go wrong) inside a function. In the subsequent lessons, we’ll talk about validating user input, and then introduce a useful tool to help document and validate assumptions.

Handling errors in functions

Functions may fail for any number of reasons -- the caller may have passed in an argument with an invalid value, or something may fail within the body of the function. For example, a function that opens a file for reading might fail if the file cannot be found.

When this happens, you have quite a few options at your disposal. There is no best way to handle an error -- it really depends on the nature of the problem and whether the problem can be fixed or not.

There are 4 general strategies that can be used:

  • Handle the error within the function
  • Pass the error back to the caller to deal with
  • Halt the program
  • Throw an exception

Handling the error within the function

If possible, the best strategy is to recover from the error in the same function in which the error occurred, so that the error can be contained and corrected without impacting any code outside the function. There are two options here: retry until successful, or cancel the operation being executed.

If the error has occurred due to something outside of the program’s control, the program can retry until success is achieved. For example, if the program requires an internet connection, and the user has lost their connection, the program may be able to display a warning and then use a loop to periodically recheck for internet connectivity. Alternatively, if the user has entered invalid input, the program can ask the user to try again, and loop until the user is successful at entering valid input. We’ll show examples of handling invalid input and using loops to retry in the next lesson (7.16 -- std::cin and handling invalid input).

An alternate strategy is just to ignore the error and/or cancel the operation. For example:

In the above example, if the user passed in an invalid value for y, we just ignore the request to print the result of the division operation. The primary challenge with doing this is that the caller or user have no way to identify that something went wrong. In such case, printing an error message can be helpful:

However, if the calling function is expecting the called function to produce a return value or some useful side-effect, then just ignoring the error may not be an option.

Passing errors back to the caller

In many cases, the error can’t reasonably be handled in the function that detects the error. For example, consider the following function:

If y is 0, what should we do? We can’t just skip the program logic, because the function needs to return some value. We shouldn’t ask the user to enter a new value for y because this is a calculation function, and introducing input routines into it may or may not be appropriate for the program calling this function.

In such cases, the best option can be to pass the error back to the caller in hopes that the caller will be able to deal with it.

How might we do that?

If the function has a void return type, it can be changed to return a Boolean that indicates success or failure. For example, instead of:

We can do this:

That way, the caller can check the return value to see if the function failed for some reason.

If the program does return a normal value, things are a little more complicated. In some cases, the full range of return values isn’t used. For example, dividing two numbers should never return a result of 0.0. In such cases, we can use a return value that wouldn’t otherwise be possible to occur normally to indicate an error:

However, if the full range of return values are needed, then using the return value to indicate an error will not be possible (because the caller would not be able to tell whether the return value is a valid value or an error value). In such a case, an out parameter (covered in lesson 10.3 -- Passing arguments by reference) might be a viable choice.

Fatal errors

If the error is so bad that the program can not continue to operate properly, this is called a non-recoverable error (also called a fatal error). In such cases, the best thing to do is terminate the program. If your code is in main() or a function called directly from main(), the best thing to do is let main() return a non-zero status code. However, if you’re deep in some nested subfunction, it may not be convenient or possible to propagate the error all the way back to main(). In such a case, a halt statement (such as std::exit()) can be used.

For example:

Exceptions

Because returning an error from a function back to the caller is complicated (and the many different ways to do so leads to inconsistency, and inconsistency leads to mistakes), C++ offers an entirely separate way to pass errors back to the caller: exceptions.

The basic idea is that when an error occurs, an exception is “thrown”. If the current function does not “catch” the error, the caller of the function has a chance to catch the error. If the caller does not catch the error, the caller’s caller has a chance to catch the error. The error progressively moves up the call stack until it is either caught and handled (at which point execution continues normally), or until main() fails to handle the error (at which point the program is terminated with an exception error).

We cover exception handling in chapter 20 of this tutorial series.


7.16 -- std::cin and handling invalid input
Index
7.14 -- Common semantic errors in C++

95 comments to 7.15 — Detecting and handling errors

  • jkwi

    The following example

    is only ok if we make an assumtion that the array contains only positive values. What if the array contained the value -1? It would be impossible for the caller to distinguigh between the error code and the return value.

  • Nirbhay

    Hi!

    In section "Handling assumption errors", the sentence after the code snippet "In the above example, if cstring is null, we don’t print anything. We have skipped the code that depends on cstring being non-null.(non-null or null?)"

    Thanks

    • Alex

      It's correct as written. The if statement only executes if cstring is not null. Thus, if cstring is null, we don't print anything, skipping over the code that would execute if cstring were non-null.

      • Nirbhay

        Hello!

        I guess I got confused with the verb 'have' as in we skipped the code already in the above snippet.

        Anyway, thanks for the clarification.

  • daniel

    You should also said that semantic error's are also called a bug

  • Alireza

    Hi dear,
    What about std::clog ?

  • Zha Weihua

    In the above example, if cstring is null, we don’t print anything. We have skipped the code that depends on cstring being non-null. This can be a good option if the statement being skipped isn’t critical and doesn’t impact the program logic. The primary challenge with doing so is that the caller or user have no way to identify that something went wrong.

    May I think you have a typo in above paragraph?

    We have skipped the code that depends on cstring being null instead of non-null?

    • Alex

      No, it's correct as written. The if statement body only executes if cstring is non-null.

      Regardless, I think it's a little hard to parse, so I'm removing the second sentence altogether, as it doesn't add much to the explanation.

  • Blackbot

    Something I noticed on my machine: While the order within cout and cerr is always the correct one, the order relative to each other is not. On my machine (using CLion), this code produces a different order of outputs every time (for example, Cerr and Cout might take turns from line to line, or the last few lines might be all Cerr and the first few lines all Cout):

    While this might be dependant on the environment it could be a good idea to mention that this depends on the IDE because it confused the hell out of me (and again when it happened in the examples of Chapter 14.3). :)

    • Hi Blackbot!

      @std::cout and @std::cerr use different output streams. Whenever you send something to it (using <<) it doesn't immediately get printed, instead it's stored in a buffer until some time later. @std::cout might decide to print the buffer before @std::cerr does, or the other way around. Using @std::endl rather than '\n' should cause a fixed order, because @std::endl causes the buffer to be printed immediately.

      * Line 4: Use uniform initialization.

      • Blackbot

        Hi nascardriver,

        unfortunately, @std::endl (and @std::flush, for that matter) does not cause fixed order for me.
        Again, this is dependant on the IDE used (when I run the same program in Terminal everything is fine with and without @std::endl) so not everybody will notice this behavior.

        I'm aware that the output streams are different, I just think it could be helpful to mention that (for someone very new to programming maybe unexpected) the streams do not necessarily keep their order relative to each other depending on the environment.

        • Jeff

          Understanding why the terminal output may not be ordered the same way as what your application did in time is, for most practical purposes, outside of the application's control.

          Your program wrote to one queue that the OS calls "stdout" and then wrote to another queue called "stderr" a fraction of a second later. From that moment on, what happens is up to the OS and  not your application. It is the OS that then chooses which queue is "more important" to process first and where to send the data, if anywhere.  For all your C++ program knows, "stderr" may be sent to  the "bit bucket" with a redirect in sh/bash such as "2 > /dev/null", in which case it just gets ignored. It might be to a file, it might be to a physical device like a serial port. It is very common in Unix-like OSes for stdout to go to stdin of another program ("fast" data transfer) and stderr to  go to a terminal or file ("slow" data transfer). Waiting for stderr might slow down stdout and all the programs that depend on it being "fast" unacceptably. It is very common to expect that a well-behaved program "never" writes to stderr (since nothing should ever go wrong). If you're in a microcontroller or embedded environment, there may not even be any place that stdout and stderr "go".

          Even with a modern "desktop/server" OS, there is never a guarantee on the order or time delays involved. That you happen to be viewing both stdout and stderr is not something that your C++ program knows. If you watch a desktop or phone app redrawing the screen in slow motion, the way that the pixels get rendered might surprise you. Those spots of light that you interpret as characters might be left to right, right to left, top to bottom, bottom to top, or whatever the graphics chip, its microcode, its drivers, and the OS libraries decide is "best".

          There are OSes that address this, such as "Real-Time Operating Systems" ("RTOS"), where the programmer has great control over both the order of events and when things happen. RTOS are more common when precise timing is important or a "guarantee" of response within a certain time are needed (such as control systems). You'd probably agree that deploying your airbag is much more important that writing a message to the console "Deploying airbag in 3 microseconds" at 9600 baud, even if the code "sent" the text before sending the trigger to the airbag.

  • Micah

    I'm not sure if it's just me but this gives me problems

    Using 'const std::array &array' doesn't work for me as a parameter, I had to change it to a vector to get the code to compile. Was that a mistake or does it work for you?

    • nascardriver

      Hi Micah!

      This is a mistake in the lesson.
      Lokesh pointed this out 2 years ago and Alex said he fixed it ( http://www.learncpp.com/cpp-tutorial/712-handling-errors-assert-cerr-exit-and-exceptions/comment-page-1/#comment-257570 ), apparently something didn't go as planned. Alex will most likely fix it in a couple of days.

      When using std::array you need to specify the data type in amount of elements.
      Example:

  • James Ray

    but it explicitly signals and intent

    an intent

  • nikos-13

    "std::cout << cstring;" is the code that depends on the assumption being valid?

  • jenifer

    What's the major difference between std::cout and std::cerr?  Both prints on console output.  Can we not just use std::cout ?

    • Alex

      In practice, very little -- however, it's possible to reroute the output of a particular channel (std::cout or std::cerr) elsewhere. In some cases, it's useful to let std::cout go to the monitor, but route std::cerr to a log file for analysis later.

      • Matias

        I have also seen large projects come up with their "Logger" classes and handle them in files. I do not know if they implement std::cerr to be directed to a specific file or not, but I am a bit curious to see if you know how they implement said Logger classes

        • Alex

          I like to use logging classes myself. Writing one of these can be a little tricky, but it's doable via a combination of static members (so you don't have to instantiate a logging object to call the logging member function) and some useful preprocessor functionality (particularly __FILE__ and __LINE__).

          Most often I'll write the log to a file, so it can be opened and examined later.

  • Ayush Goel

    Hey Alex, can you please explain the starred lines, i didn't get it???

    if (std::cin.fail())
            {
                std::cin.clear(); // reset any error flags
          ***** std::cin.ignore(32767, '\n'); // ignore any characters in the input buffer
          ***** index = -1; // ensure index has an invalid value so the loop doesn't terminate
                continue; // this continue may seem extraneous, but it explicitly signals and intent to terminate this loop iteration
            }

    • Alex

      std::cin will hold input that hasn't been extracted yet, so it can be extracted later. In the case of invalid input, this input that hasn't been extracted can cause problems.

      The std::cin.ignore line gets rid of all the characters up to (and including) a '\n'. In cases where lines of input are terminated with a '\n' (which is all user input), this should clear any extra characters out of std::cin, so we can start from a fresh state (where std::cin is not holding any characters).

      The index = -1 is just a convenient way to keep looping that's specific to the example.

  • Matt

    I am curious about assert() being a preprocessor macro... because it seems to look and act just like any other c++ code as far as I can see. What makes it a preprocessor macro, and why must it be that way?

    • Alex

      Remember that the preprocessor just does text substitution. Because assert() is a macro, the preprocessor will replace any call to macro assert() with a call to a real assert function (with a similar name, like _assert()), along with 3 arguments: the expression being asserted, the current filename, and the current line number.

      assert() is implemented as a macro because the C++ specification requires it to be implemented that way. :) The why question is more interesting. The short answer is: because the macro can generate the arguments for us.

      Consider what would happen if you wrote assert() as a function. In this case, whenever the user called the assert() function, they would have to explicitly pass in: the expression to be evaluated, the expression to be evaluated as a string (so the assert can print the expression if the assertion fails), the current filename, and the current line number. What a pain that would be!

      Now, you might think, why not just have the assert() function determine the current filename and line number itself? The problem is that the assert() function can't ask for the current filename or line number itself until it's already been called -- and at that point, the current filename and line number have already been changed by virtue of calling the assert function! There's also no easy way to get a string version of the assertion expression.

      So, in short, assert() is super easy to implement as a macro, and either super hard to implement as a function, or super burdensome on the user to implement as a function.

  • Matt

    I think there's a minor typo in the commented code above section "Handling assumption errors", on line number 20. I believe that "and" should be "an".

  • Chris

    Alex,

    at handling assumptions errors section number 5 comment code line 3:

    "Only print if strString is non-null" strString? it is typo? it should be cstring, isn't it?

  • Nyap

    with this example

    how could you enter a null pointer
    do escape sequences work during runtime?

    • Alex

      would try to print a nullptr.

      Escape sequences may work at runtime, but it depends on how your console interprets them.

  • Rob G.

    No compiler version I use resolves this error. Just making a null pointer snippet to test. Is this operator error or otherwise?

    error:
    .\src\main.cpp:6:13: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
      if(ptr != -1)

    • Alex

      The problem is this line:

      You're comparing a pointer value (which holds a memory address) to a non-zero integer. This is not allowed. The only valid integer you can compare a pointer to is 0 (null).

      Are you sure you didn't mean this?

      • Rob G.

        Yes, meant that.
        if(*ptr != -1) // if the value that ptr is pointing to is -1
        So dereferencing the pointer removes the violation. *ptr = int? Got it.

        Thought I didn't violate the condition:
        "The only valid integer you can compare a pointer to is 0 (null)."
        I set ptr to nullptr, passed through fx test. I compare nullptr(ptr) to -1.
        So didn't I compare a valid int (-1) to a null ptr(ptr)?

  • Chaithu. K.C.K.

    How the c compiler will check the syntax, semantic  errors ?

  • Bernhard

    Hello! I found a small mistake in one of the code examples (I posted a bit of the rest of the code with it to provide context):

    "input" in line 19 of the original code you posted should be "index", there is no variable "input".

    Once again, thank you for the great tutorial :)

  • Evaldas

    Hey,
    How come in the index input example, after the std::cin.fail() the continue command doesn't work for me?
    I've tried replacing it with an std::cout and it printed me it, but when I use continue, the do while loop just breaks, like it would count index as 0 and move on. Is it supposed to be like that?

    • Alex

      No, it's not supposed to be like that. I'm not sure why you'd be seeing that behavior.

      • Evaldas

        Continue doesn't seem to work at all for me in do while loops.. I'm using Code::Blocks. I've tried writing even a simple program and it would just terminate the program for some reason.

        If I input 6, it just terminates the loop.
        I've tried it with a while as well.

        It terminates as well for some reason.

        • Alex

          The behavior in the second case makes sense -- when you continue, you're going back up to the top of the loop. At that point, number == 6, and while (6 < 5) evaluates to while (false), so the loop terminates. The first one is a little more perplexing. It turns out that inside a do while loop, continue takes you to the bottom of the loop, not the top! So the same thing is happening here. I actually didn't realize this, so you just taught me something new. :)

          • Shiva

            Yes, that example is broken. As far as I know continue can't  jump past the loop condition; it skips only the rest of the statements inside the loop body, and moves the control to the loop condition for the next iteration. So the continue statement in this example does nothing. Now, even if std::cin fails, it will (somehow) read 0 into index (you can verify this by printing index after each extraction).

            So even if I enter a non-integer, it will pass the loop condition and break out of it, and print Letter #0 ('H') for me. That's not what the programmer intended - a semantic error in the chapter about semantic errors? ;)

            I deleted the continue statement and replaced it with:

            Now it works as expected.

            The same example comes twice in the lesson, I hope Alex will update both. :)

            • Alex

              Although the continue in the example above is extraneous from a functionality perspective (because the loop ends immediately after anyway), in my mind, it's more correct to have it than not. The intent of the nested block is to restart the iteration after executing -- therefore, it's more correct to do this explicitly than rely upon the loop ending naturally to do this implicitly.

              You're right that the example has an error, since the value of index could be undefined when the loop condition evaluates if the error condition triggered. I'll fix the example. :)

            • Shiva

              Totally agree. Boy, I was the guy who asked you how I wanted to have a continue statement in a default switch-case (I said including that made the logic clearer), and you told me to go with it if I liked it that way, remember?. Of course it doesn't appear extraneous to me.

              The reason why I pointed it out is, I imagined you to be still thinking that continue would make the control jump to the top of the do block, while it actually goes to the bottom. Looks like you were not. :D

              Anyway good update. Instead of replacing the continue like I did, you added index = -1; above it. This way is better. But you misnamed the variable, as reader Bernhard rightly spotted, please correct it as well. :)

      • Evaldas

        Perhaps I’ve made a mistake rewriting everything in the do while? I’m not sure.

  • Eyad

    i know its not related to the content but i have to ask this question......
    can i click a random bunch of ads to support this site, then enable the adblock :D ?

  • Connor

    Hello again Alex, I've a couple questions:

    1. I understand how to handle an error of a null pointer passed to a function but how would you handle a dangling pointer? Is there a way to check if a pointer is dangling vs only null?

    2. With respect to

    why do we use:

    after? Also I still don't understand why 32767 - is this the max size of a standard buffer?

    3. With respect to using

    for an int; if I enter a char type cin.fail is true (so I assume it doesn't cast the char to type int) but if I enter a float or double it takes the value in front of the '.', is there a way to handle a float -> int error?

    4. How/where do we view the

    error sent to the OS?

    5. How is cerr any different than cout?

    6. For the assert() piece, as well as

    , for this example we also have to

    . It took me a while to figure this out lol.

    7. Finally, back in 2009 you mentioned something about wanting to do a book, well I'll be the first to buy it! WHEN CAN WE BUY YOUR BOOK!!!!!???

  • wildbartty

    It might be a good idea to update the if statements so that they use proper syntax

  • Lokesh

    In the assert statement code example, there is no type or size of std::array. It produces a compile time error:
    argument list for class template "std::array" is missing
    I think it should be:

    since its checking between index 0 and 9, where "type" is the data type of the array.

  • Len

    Hi Alex,
    Assert is fantastic, I had never heard of this before.
    What about the try/catch keywords?  This is normally what I use (although now I see that "assert" is often a better choice).

    • Alex

      Assert and try/catch are really for different things.

      Try/catch is best when you expect something to succeed, but want to cover the case where it may not.

      Assert is best when you need to ensure something is true before you do something else.

      I cover try/catch later, in the lesson on exception handling.

  • Mr D

    Hi Alex,

    You're using the word "user" to describe both the computer programmer AND the end user of the program, which is a bit confusing!! Why not use "programmer" instead?!

    • Alex

      Good comment. I've updated the language slightly to help clarify when I'm talking about the end-user and when I'm talking about the programmer or program.

  • cpplx

    liberally - i dont like the word, consider using more appropriate one.

    does using asserts "liberally" create overhead?
    in other words, does it take time to check the condition?

    • Alex

      Yes, using assert has an overhead. Consequently, many people make them conditional so they're only compiled into debug builds. That way if somethings goes wrong during testing, the assert will (hopefully) fire, but your production code won't have the performance hit.

      • Pablo

        This is perhaps an advanced topic, but how do you make part of your code compile only in debug builds?

        • Alex

          Easiest way is to use the preprocessor:

          (and make sure that DEBUG is defined only for your debug build only).

          Visual Studio defines _DEBUG automatically as part of default debug builds, so you can use that if you want.

          • Pablo

            I think it will be a long time until I can really take advantage of this, but good to know! (Also, it is good to know a bit of these more advanced features, in case you have to read other people's code).

            As always, thank you. It is impressive the way you keep up to date with the comments!

  • newUser

    I'm still wondering how a good assert should be written. Like should it be written a line above a for statement while copying the statement's arguements? If this is correct, might we want to put this in the tutorial as a "best practices" while giving context to how it's written?

    • Alex

      As noted in the article, assert statements are typically used at the top of functions to check that the parameters are valid, as well as the function return value, to ensure an error didn't occur. Assert is only appropriate if the condition can't be fixed (e.g. by asking the user to try again).

  • To answer that question Michel, it's good practice to put your own error handling in where ever possible.

    The error handling can be used to put the errors into terms that the end user can understand, not just the programmer.

    Error handling is not just good for debugging code, it's also good to help point out errors in live programs being used by a client or user.

    Alex THANK YOU FOR THIS ARTICLE!! I just sent it to a friend of mine who needed to see this, shall we say. ;)

  • On Visual Studio 2008 Professional Edition, when an error is created, it states all the information in the Error List including which file and which line in that file the error occured. Therefore, do you need to use the assert() function when you are using VS 08 Pro?

    • Alex

      Yes. Your compiler will only catch syntax errors during compile time. Semantic errors and other runtime errors are your responsibility to detect and handle.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]