C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
101: Error Codes vs Exceptions
I was digging through some legacy code the other day and found a function that handled user authentication. It was a mess. Every single function call was wrapped in an if statement checking an integer return value. It looked more like a staircase than actual logic. Let's recreate that frustration so we can figure out a better way to handle things.
The "Check Everything" Approach
Imagine we're building a simple system to read a configuration file. We need to open the file, find a specific key, and convert the value to an integer. In the old-school C style—which still pops up in C++ all the time—we'd use error codes. I'll try to implement this the "safe" way first.
int readConfigValue(const std::string& key, int& outValue) {
if (!openFile("config.txt")) {
return -1; // File not found
}
std::string valStr;
if (!findKey(key, valStr)) {
return -2; // Key not found
}
if (!parseInteger(valStr, outValue)) {
return -3; // Not a number
}
return 0; // Success!
}
At first glance, this seems fine. It's explicit. But look at how I have to use it in the main logic. I can't just "get the value"; I have to manage the failure of every single step.
int main() {
int port;
int result = readConfigValue("server_port", port);
if (result == -1) {
std::cerr << "Could not open file" << std::endl;
} else if (result == -2) {
std::cerr << "Port key missing" << std::endl;
} else if (result == -3) {
std::cerr << "Port is not a valid number" << std::endl;
} else {
std::cout << "Connecting to port: " < < port < < std::endl;
}
}
The Boilerplate Bloat
Here is where I hit a wall. What if readConfigValue was called by another function, which was called by another? I'd be passing those -1s and -2s all the way up the stack. I'd spend 70% of my time writing if (result != 0) return result;. It's tedious, and more importantly, it's easy to forget one check. If I forget to check a return code, the program just keeps running with garbage data, and I spend three hours debugging a segmentation fault that happened ten function calls ago.
Letting it Fly
I want to separate the "happy path" (the logic that happens when things work) from the "error path." This is where exceptions come in. Instead of returning a code and hoping the caller handles it, I'm going to just throw the problem up the chain. Let's rewrite that same logic.
int readConfigValue(const std::string& key) {
if (!openFile("config.txt")) {
throw std::runtime_error("Configuration file missing");
}
std::string valStr;
if (!findKey(key, valStr)) {
throw std::invalid_argument("Key not found in config: " + key);
}
return std::stoi(valStr); // std::stoi throws its own exception if it fails!
}
Look at the difference. The function now returns the actual value I want, not a status code. The "happy path" is a straight line. If something goes wrong, the execution jumps immediately to the nearest catch block, skipping all the intermediate steps that can't possibly fix the error anyway.
Dealing with the Fallout
Now, I can't just ignore the errors. I'll wrap the call in a try-catch block. This centralizes the error handling in one place.
int main() {
try {
int port = readConfigValue("server_port");
std::cout << "Connecting to port: " < < port < < std::endl;
} catch (const std::exception& e) {
std::cerr << "Config Error: " < < e.what() < < std::endl;
return 1;
}
}
But wait—is this always better? Not necessarily. Exceptions are "expensive" in terms of performance when they are actually thrown. If you're writing a high-frequency trading bot or a physics engine where "failure" happens thousands of times a second (like a collision check), throw will kill your performance. In those cases, I'd actually go back to error codes or use std::optional.
The rule of thumb I use: Use exceptions for exceptional circumstances—things that shouldn't happen in a healthy system (like a missing config file or a lost database connection). Use return codes for expected control flow (like "user entered an invalid password").
📋 Practical Task
Refactoring the Database Connection Manager
You have been handed a legacy DatabaseManager class that uses C-style integer return codes to signal failure. This has led to "silent failures" where the application continues to run even after the database connection has dropped.
Your Task:
Refactor the provided connect() and executeQuery() methods. Instead of returning 0 for success and -1 or -2 for failure, modify them to throw std::runtime_error with descriptive messages. Finally, update the main() function to use a try-catch block to handle these errors gracefully.
// Current Legacy Code
class DatabaseManager {
public:
int connect() {
// Simulate failure
return -1;
}
int executeQuery(std::string query) {
// Simulate failure
return -2;
}
};
int main() {
DatabaseManager db;
if (db.connect() == 0) {
if (db.executeQuery("SELECT * FROM users") == 0) {
// Success
} else {
std::cout << "Query failed";
}
} else {
std::cout << "Connection failed";
}
return 0;
}There are no comments for now.