Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Why Exception Handling is Necessary
Learn error handling techniques and why exceptions are needed for robust programs.
In previous lessons on error handling, you learned about using assertions, error logging with std::cerr, and program termination with exit(). While these techniques have their uses, they don't provide a complete solution for error handling in larger programs. This lesson explores why traditional error-handling approaches fall short and introduces exceptions as a better alternative.
Return codes and their limitations
Return codes are a traditional method for signaling errors. Consider a function that searches for a configuration value:
#include <string_view>
int findSetting(std::string_view config, std::string_view key)
{
for (std::size_t i{0}; i < config.length(); ++i)
{
if (config.substr(i, key.length()) == key)
return static_cast<int>(i);
}
return -1; // Not found
}
This function returns the position where a key appears in a configuration string, or -1 if not found. While simple, this approach has significant problems:
Problem 1: Ambiguous return values
Is -1 an error, or could it be a valid return value? Without checking documentation or the implementation, it's impossible to know. Consider:
int calculateOffset(int base, int adjustment)
{
return base - adjustment;
}
If this returns -1, is that an error or a valid result? Return codes lack clarity.
Problem 2: Functions need to return both results and errors
What if a function must return a useful value and also signal potential errors? Consider a configuration parser:
double parseNumber(std::string_view text)
{
return std::stod(std::string{text});
}
This function needs to return the parsed number, but what if the text isn't a valid number? You can't use the return value for both purposes, forcing awkward workarounds:
#include <iostream>
#include <string>
double parseNumber(std::string_view text, bool& success)
{
try
{
success = true;
return std::stod(std::string{text});
}
catch (...)
{
success = false;
return 0.0;
}
}
int main()
{
bool success{};
double value{parseNumber("3.14", success)};
if (!success)
{
std::cerr << "Parse error\n";
return 1;
}
std::cout << "Parsed: " << value << '\n';
return 0;
}
This is verbose and error-prone. Callers must remember to check the success flag before using the result.
Problem 3: Error checking clutters code
When many operations can fail, error checking dominates the code:
#include <iostream>
#include <fstream>
#include <string>
bool loadConfiguration(const std::string& filename,
int& width,
int& height,
std::string& title)
{
std::ifstream file{filename};
if (!file)
{
std::cerr << "Cannot open file: " << filename << '\n';
return false;
}
if (!(file >> width))
{
std::cerr << "Cannot read width\n";
return false;
}
if (!(file >> height))
{
std::cerr << "Cannot read height\n";
return false;
}
if (!std::getline(file, title))
{
std::cerr << "Cannot read title\n";
return false;
}
return true;
}
int main()
{
int width{}, height{};
std::string title;
if (!loadConfiguration("settings.cfg", width, height, title))
{
std::cerr << "Configuration loading failed\n";
return 1;
}
std::cout << "Size: " << width << "x" << height << '\n';
std::cout << "Title: " << title << '\n';
return 0;
}
The actual work (reading three values) is buried under error checking. Each read operation requires an if statement and an error return. Imagine if there were twenty configuration values - the code becomes unreadable.
Problem 4: Constructors can't return error codes
Classes often need to perform initialization that can fail. But constructors don't have return values:
class DatabaseConnection
{
private:
int m_socket;
public:
DatabaseConnection(const std::string& host, int port)
{
m_socket = connectToDatabase(host, port);
// What if connection fails?
// Can't return an error code!
}
};
You could pass an error flag by reference:
class DatabaseConnection
{
private:
int m_socket;
bool m_connected;
public:
DatabaseConnection(const std::string& host, int port, bool& success)
: m_socket{connectToDatabase(host, port)}
, m_connected{m_socket >= 0}
{
success = m_connected;
}
bool isConnected() const { return m_connected; }
};
But this is messy, and the object still gets created even if construction failed. Now you have a partially-initialized object to deal with.
Problem 5: Error propagation is cumbersome
When a low-level function encounters an error, it often needs to propagate that error up through multiple layers:
bool readConfig(const std::string& filename, Config& config)
{
if (!openFile(filename))
return false;
if (!parseHeader(config))
return false;
if (!parseSettings(config))
return false;
return true;
}
bool initialize(Config& config)
{
if (!readConfig("app.cfg", config))
return false;
if (!validateConfig(config))
return false;
return true;
}
int main()
{
Config config;
if (!initialize(config))
{
std::cerr << "Initialization failed\n";
return 1;
}
// Use config...
return 0;
}
Every function in the chain must check for errors and propagate them upward. This creates a rigid error-handling structure that constrains how you organize code.
The fundamental problem with return codes
The core issue is that return codes mix normal program flow with error-handling flow. Every function that might fail:
- Must dedicate its return value to signaling success/failure
- Forces callers to check the return value before proceeding
- Creates a tightly coupled chain of error checking
- Makes it difficult to separate "what the code does" from "how it handles errors"
This intermingling of concerns makes code harder to read, write, and maintain.
What we need: Separation of concerns
Ideally, we want to:
- Write the normal execution path clearly
- Handle errors separately from normal flow
- Allow errors to propagate automatically when appropriate
- Let different parts of the program handle errors in different ways
- Signal errors from constructors and other special functions
Traditional return codes can't provide these capabilities. We need a different mechanism.
Enter exceptions
Exceptions provide a way to signal and handle errors that decouples error handling from normal control flow. Instead of:
if (!operationThatMightFail())
{
// Handle error
}
else
{
// Continue normal execution
}
You write:
try
{
operationThatMightFail(); // Might throw an exception
// Continue normal execution
}
catch (...)
{
// Handle error
}
The key difference: The error-handling code is separated from the normal execution path. Functions can signal errors without consuming their return values. Errors can propagate automatically through multiple function calls until they reach code that knows how to handle them.
In the next lesson, we'll explore how to use exceptions in C++ using the try, throw, and catch keywords.
Why Exception Handling is Necessary - Quiz
Test your understanding of the lesson.
Practice Exercises
Basic Exception Handling
Convert a function that uses return codes for error handling to use exceptions instead. Create a divide() function that throws an exception when dividing by zero.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!