In the previous lesson on handling errors, we talked about ways to use assert(), cerr(), and exit() to handle errors. However, we punted on one further topic that we will now cover: exceptions.
When return codes fail
When writing reusable code, error handling is a necessity. One of the most common ways to handle potential errors is via return codes. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int findFirstChar(const char* string, char ch) { const std::size_t stringlength{ strlen(string) }; // Step through each character in string for (std::size_t index=0; index < stringlength ; ++index) // If the character matches ch, return its index if (string[index] == ch) return index; // If no match was found, return -1 return -1; } |
This function returns the index of the first character matching ch within string. If the character can not be found, the function returns -1 as an error indicator.
The primary virtue of this approach is that it is extremely simple. However, using return codes has a number of drawbacks which can quickly become apparent when used in non-trivial cases:
First, return values can be cryptic -- if a function returns -1, is it trying to indicate an error, or is that actually a valid return value? It’s often hard to tell without digging into the guts of the function.
Second, functions can only return one value, so what happens when you need to return both a function result and an error code? Consider the following function:
1 2 3 4 |
double divide(int x, int y) { return static_cast<double>(x)/y; } |
This function is in desperate need of some error handling, because it will crash if the user passes in 0 for parameter y. However, it also needs to return the result of x/y. How can it do both? The most common answer is that either the result or the error handling will have to be passed back as a reference parameter, which makes for ugly code that is less convenient to use. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> double divide(int x, int y, bool &success) { if (y == 0) { success = false; return 0.0; } success = true; return static_cast<double>(x)/y; } int main() { bool success; // we must now pass in a bool value to see if the call was successful double result = divide(5, 3, success); if (!success) // and check it before we use the result std::cerr << "An error occurred" << std::endl; else cout << "The answer is " << result << '\n'; } |
Third, in sequences of code where many things can go wrong, error codes have to be checked constantly. Consider the following snippet of code that involves parsing a text file for values that are supposed to be there:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
std::ifstream setupIni("setup.ini"); // open setup.ini for reading // If the file couldn't be opened (e.g. because it was missing) return some error enum if (!setupIni) return ERROR_OPENING_FILE; // Now read a bunch of values from a file if (!readIntegerFromFile(setupIni, m_firstParameter)) // try to read an integer from the file return ERROR_READING_VALUE; // Return enum value indicating value couldn't be read if (!readDoubleFromFile(setupIni, m_secondParameter)) // try to read a double from the file return ERROR_READING_VALUE; if (!readFloatFromFile(setupIni, m_thirdParameter)) // try to read a float from the file return ERROR_READING_VALUE; |
We haven’t covered file access yet, so don’t worry if you don’t understand how the above works -- just note the fact that every call requires an error-check and return back to the caller. Now imagine if there were twenty parameters of differing types -- you’re essentially checking for an error and returning ERROR_READING_VALUE twenty times! All of this error checking and returning values makes determining what the function is trying to do much harder to discern.
Fourth, return codes do not mix with constructors very well. What happens if you’re creating an object and something inside the constructor goes catastrophically wrong? Constructors have no return type to pass back a status indicator, and passing one back via a reference parameter is messy and must be explicitly checked. Furthermore, even if you do this, the object will still be created and then has to be dealt with or disposed of.
Finally, when an error code is returned to the caller, the caller may not always be equipped to handle the error. If the caller doesn’t want to handle the error, it either has to ignore it (in which case it will be lost forever), or return the error up the stack to the function that called it. This can be messy and lead to many of the same issues noted above.
To summarize, the primary issue with return codes is that the error handling code ends up intricately linked to the normal control flow of the code. This in turn ends up constraining both how the code is laid out, and how errors can be reasonably handled.
Exceptions
Exception handling provides a mechanism to decouple handling of errors or other exceptional circumstances from the typical control flow of your code. This allows more freedom to handle errors when and how ever is most useful for a given situation, alleviating many (if not all) of the messiness that return codes cause.
In the next lesson, we’ll take a look at how exceptions work in C++.
![]() |
![]() |
![]() |
>>If the caller doesn’t want to handle the error, it either has to ignore it (in which case it will be lost forever), or return the error up the stack to the function that called it.
I kind of lost in the second part, "or return the error up the stack to the function that called it. "
it means for example a function named A is called by main() and this function produces an error code as a return code but main() is not equipped to handle that return value(error) so that value will be returned up the stack to the A again? if so, how come?
In this context, "up the stack" means "towards main". Put another way, "passed back to the caller".
Hey I almost finished your full C++ courses, i'm here today at the exception chapter, and I got a question :
When should you use exception ?
When should you use assertion ?
what is the difference between both ?
If you have time can you provide an example !
Thanks in advance, I have learnt so much on your website as a student !
> When should you use exception
When an error occurs. But you should design your program such that errors are unlikely to occur.
> When should you use assertion
Assertions are for development, you shouldn't ship a program with assertions enabled. Assert everywhere where you think you know what happens, but you're worried you might did something wrong.
> what is the difference between both
You can catch exceptions and handle the error. `assert` terminates your program.
> If you have time can you provide an example
There are examples in the lessons. If you have questions about a specific one, feel free to ask.
Hello, in the following code:
do we have to use 'static_cast' in the divide() function?
Because we are returing a 'double' from the divide function, won't the return value (let's say an integer) be converted automatically to double??
Thanks!
Without the cast, the division is performed on integers.
If at least one of the operands is a `double`, the division is performed on floating points.
Clear! thanks!
Thank you for your great content Alex. But I have a question, why we don't just handle message error directly in function like:
double divide(int x, int y)
{ if(y == 0){
cout<<"Y must be not zero";
}else
return static_cast<double>(x)/y;
}
Maybe the caller doesn't want a message to be printed. Or they want to print a different message depending on where @divide is being called from.
Hi, I know this is not the focus of this lecture, however in the first code snippet, strlen(string) should be done just once, before the loop, not in every iteration.
Updated, and fixed the signed/unsigned mismatch too. Thanks for the suggestion!
Alex, please enjoy some of my typo detection services :-)
"This in turns ends up constraining" >> in turn ends up constraining
All of this error checking and returning values makes determining that the function is trying to do much harder to discern.
determining what
Thanks for pointing out the myriad of typos. All fixed!
You're welcome!
If the charater matches ch, return its index
character
Typo found:
Furthermore, even if you do this, the object will still be created and then has to be dealt with or disposed *off.
Believe should be 'off', and not 'of'.
Btw... thanks A LOT Sir for the tutorials (been managing them EXTREMELY WELL for... about 10 years now? That requires some serious dedication, Sir!) May God bless you!
In fact, I would go as far as to say that these are the best tutorials, on ANYTHING, ever!
Correct English phrasing is "disposed of". And you're welcome. Glad you're enjoying the tutorials.
You're right. (Well that was embarrassing...)
So sorry, and thank you.
Typo found.
"Consider the following snuippet of code that involves parsing a text file for values that are supposed to be there:"
Is it snippet?
Yes, snippet. Extraneous u removed.
Alex,
I don't understand your ifstream example, and what these missing parameters are that you are referring to. Could you explain in more detail what is going on here? Thanks.
The code in the example is meant to illustrate a case where we're reading some integer values from a file. If the values aren't in the file, or are in the wrong format, an error code is returned to the caller.
I've updated the function name, and added some comments about the example. Does that help clarify?
HI Alex,
You are awesome i thought i should drop out from the college after getting to know my in ability to code but because of you I am still studying.
Thank you
Glad to hear it. Study on!
Hi Alex,
Thank you for the great tutorial. I like it because you not only explain what and how, but more importantly, you explain why.