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 function returns a normal value, things are a little more complicated. In some cases, the full range of return values isn’t used. In such cases, we can use a return value that wouldn’t otherwise be possible to occur normally to indicate an error. For example, consider the following function:

The reciprocal of some number x is defined as 1/x, and a number multiplied by its reciprocal equals 1.

However, what happens if the user calls this function as reciprocal(0.0)? We get a divide by zero error and a program crash, so clearly we should protect against this case. But this function must return a double value, so what value should we return? It turns out that this function will never produce 0.0 as a legitimate result, so we can return 0.0 to indicate an error case.

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 11.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:


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
7.14 -- Common semantic errors in C++

98 comments to 7.15 — Detecting and handling errors

  • DivisionMan

    Checking for an error with:

    will cause the error handling block to run since 0 / 5 evaulates (correctly) to 0.0, so this shouldn't be used as an error code.

  • Cuyler

    Grammatical error on the third line of text: "due occur to" should be "occur due to".

  • Person

    Typo under "Passing errors back to caller":

    "If the function has a void return type, it can changed to return a Boolean that indicates success or failure."

    "...type, it can *be* changed..."

  • Dmitri

    In the part about passing errors back to the caller by returning an invalid value, it says:

        "dividing two numbers should never return a result of 0.0."

    ..but 0.0 is a perfectly valid result for the division.  Dividing *by* zero is undefined, but dividing zero by something non-zero isn't.  Maybe a different example would be better?

    • Jamie

      0.0 could still be used as an error return, but it's sketchy:

      A more straightforward example would be a function that takes a string and returns the string with a character appended:

  • Waldo Lemmer

    1. About this:

    I don't think it's appropriate for a function to pass an *error message* to stderr when it's passing an *error code* back to the calling function. The user doesn't have to see this *error message*. The calling function should handle the *error code* instead, so that it can print an error message that's more appropriate for the user. For example:

    The error message at line 59 could confuse the user, since he/she doesn't know how the program is written (he/she might not even know how probability is calculated). When the error code gets passed to @main(), it is used to send a more friendly message (line 29).

    2. Section "Fatal errors":
    > If the error is so bad that the program can not continue to operate properly,
    "can not" should be "cannot"

    1. How does my program look? Do you think there's something I could've done better? :)
    2. Most websites warn you that text you've entered will be lost when you try to close a tab. This website doesn't, pls fix. Re-typing a comment from a ShadowPlay recording is annoying xD

    • nascardriver

      Given the name "printDivision", the function is meant to print something. It'd be confusing if the function returned without printing anything.

      Your `divide()` function on the other hand should not print, it should perform a division, because it's called "divide". `-1.1` is a valid result of a division, there's no reason for the caller to think that `-1.1` means that an error occurred. You can return a `std::optional` if your function may or may not produce a return value. Once you learn about exceptions, you can move the error message back into `divide()` if you like to.

      "nA" is a poor name.

      • Waldo lemmer

        > Given the name "printDivision", the function is meant to print something.
        Good point, I didn't consider that.

        > You can return a `std::optional` if your function may or may not produce a return value
        Thanks for telling me about this, I'll look it up :)

        > Once you learn about exceptions
        Can't wait ^^

        > "nA" is a poor name
        Yeah, I gotta admit, I could've chosen much better names lol (I used abbreviations that I learned in maths, but I should've used descriptive names instead).


  • Aryan

    Just for fun i made an easier way to get input for future lessons



  • saMira

    >>And here’s the version that checks the user input to make sure it is valid:
    I was wondering why on the following code you didn't include the following statement, while in the solution 4), you added that.

  • mqu

    Something's wrong with the code snippets, making it unreadable.

  • rustart

    in the example
    4) If the user has entered invalid input, ask the user to enter the input again.
    Shouldn't the std::cin.ignore() be called even if the std::cin didn't fail? The user may input something like 100 100, and the next cin extraction would just immediately extract the next 100 and an extra std::cout got printed to the user

    sorry for bad english

  • Gacrux

    The hello world example needs static_cast<size_t>(index) or won't compile here. Weird.

    C:\Users\admin\Documents\cbprojects\7.12\main.cpp|24|error: comparison of integer expressions of different signedness: 'int' and 'std::__cxx11::basic_string<char>::size_type' {aka 'long long unsigned int'} [-Werror=sign-compare]|

    C:\Users\admin\Documents\cbprojects\7.12\main.cpp|24|error: conversion to 'std::__cxx11::basic_string<char>::size_type' {aka 'long long unsigned int'} from 'int' may change the sign of the result [-Werror=sign-conversion]|

    C:\Users\admin\Documents\cbprojects\7.12\main.cpp|27|error: conversion to 'std::__cxx11::basic_string<char>::size_type' {aka 'long long unsigned int'} from 'int' may change the sign of the result [-Werror=sign-conversion]|

  • Samira Ferdi

    Hi, Alex and Nascardriver! How to handle if the argument that we passed to the function printString() is const or non-const char pointer?

    • I don't understand your question. Can you provide example calls and an explanation what you want to happen?

      • Samira Ferdi

        • `std::cout<<` prints everything until it finds a null-terminator. A single `char` doesn't have a zero-terminator, so `std::cout<<` continues reading from the memory after `ch`, causing undefined behavior. You can't detect this. You function name and documentation should clarify what the function does with its parameters.

  • Samira Ferdi

    Hi, Alex and Nascardriver!

    I wanna ask help to you to clarify my understanding about Alex's code

    I try to understand what this part means

    So, my understanding is like this: so, what Alex basically did is to strongly enforce the user to enter an integer. If user enter non-integers, the user must entering again and the consequences of this enforcement is the while loop part is never execute if user entering non-integers and that's basically mean we don't have to do useless non-integers comparing because it's useless in this program context.
    So, they are basically like Alex said, two steps of checking: enter an integer first, and checking and handle case where user entered an out of range integer

    Is my understanding correct?

    • `continue` doesn't skip the condition check. This code would do the same if you removed line 20.
      Alex uses `index = -1`, because if extraction fails, `index` will be set to `0`. If `index` is `0` in line 23, the loop will stop, which is not what we want. By setting `index` to `-1`, Alex makes sure that the condition is `true`.
      If the loop's condition is changed, this trick might not work anymore. I'd use a `while (true)` loop and `break` if `index` is valid.

      • Samira Ferdi

        Hi, Nascardriver! Thanks for your great explanation!

        So, using while loop is like this:

        So, there is no need of using continue and index = -1 here.

Leave a Comment

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