Search⌘ K
AI Features

Error Handling and Exceptions

Explore how to manage runtime errors in C++ by using exceptions effectively. Understand the try, throw, and catch workflow, standard exception types, and the noexcept specifier to write stable and clean code.

Errors are an inevitable part of software development. Files may be missing, network connections can fail, and users may provide invalid input. If these possibilities are not anticipated, programs become fragile and susceptible to crashes.

In C++, robust software is built by explicitly planning for such failures. Rather than embedding extensive conditional checks throughout core logic, the language provides exceptions as a structured error-handling mechanism. Exceptions allow normal execution paths, where operations succeed, to remain clear and readable, while error-handling logic is centralized and handled separately. By using exceptions effectively, we can report errors accurately and respond to them in a controlled manner, helping ensure that applications remain stable even when unexpected conditions arise.

Understanding exceptions

Exception handling in C++ is a structured way to detect, signal, and manage runtime errors. An exception is an object created and thrown by a program to indicate that an error has occurred and cannot be handled at the current point of execution. When an exception is thrown, the normal flow of the program is immediately interrupted. The runtime system then searches for a corresponding block of code designed to handle, or catch, that exception.

This search process is known as stack unwinding. When a function throws an exception and does not handle it, the exception propagates to the calling function. This process continues up the call stack until a suitable exception handler is found or the program terminates.

This mechanism is powerful because it prevents errors from being silently ignored. If an exception is not handled, the program fails explicitly, drawing attention to the problem. This behavior helps avoid continued execution in an invalid or corrupted state, making programs easier to debug and more reliable.

The try, throw, and catch workflow

C++ provides three language keywords for working with exceptions:

  • throw: This is used to signal that an error has occurred. While nearly any type can be thrown, exceptions are typically represented as objects.

  • try: This is used to enclose code that may generate an exception. This indicates that the enclosed statements should be monitored for error conditions.

  • catch: This is used immediately after a try block to define handlers for specific exception types.

Together, these keywords allow programs to detect errors, transfer control to appropriate handlers, and respond to exceptional conditions in a structured manner. To illustrate this mechanism, consider a simple example in which we attempt to divide by zero. For this initial demonstration, we will throw a basic integer error code.

C++ 23
#include <iostream>
double divide(double numerator, double denominator) {
if (denominator == 0) {
// Signal an error by throwing a primitive value (an integer)
throw 404;
}
return numerator / denominator;
}
int main() {
double a = 10.0;
double b = 0.0;
try {
// We attempt the risky operation inside the try block
std::cout << "Attempting division..." << std::endl;
// If divide() throws, execution jumps IMMEDIATELY to the catch block
double result = divide(a, b);
// This line is skipped if an error occurs
std::cout << "Result: " << result << std::endl;
}
catch (int error_code) {
// We catch the integer thrown above
std::cerr << "Error: Division failed with code " << error_code << std::endl;
}
std::cout << "Program continues safely." << std::endl;
return 0;
}
  • Lines 11–13 (main starts): Execution begins. We define our variables a and b, setting b to 0.0.

  • Line 15 (try block): We enter the try block. The compiler notes that if anything goes wrong here, it should look for a matching catch block below.

  • Line 17: We print "Attempting division...".

  • Line 20 (The Call): We call divide(10.0, 0.0). Execution jumps up to the divide function at line 3.

  • Lines 4–6 (Inside divide): The function checks if denominator is 0. Since b is 0, the condition is true. We execute throw 404;.

    • Crucial step: The function divide immediately stops. It does not return a value. The execution flow jumps straight out of the function, looking for a handler.

  • Lines 22 (Back in main): Because an exception was thrown, the assignment to result (line 20) is aborted, and the print statement on line 23 is skipped entirely.

  • Line 25 (catch block): The program finds the catch (int error_code) block. The thrown value (404) is assigned to error_code.

  • Line 27: We execute the error-handling code, printing the error message to std::cerr.

  • Line 30: Once the catch block finishes, the program resumes normal execution, printing "Program continues safely."

Standard exception types

Although throwing simple values such as integers is possible, this approach provides little descriptive information about the error. In professional C++ code, exceptions are typically thrown as objects derived from the standard library’s std::exception class. This practice is especially relevant when working with the standard data structures introduced earlier in this module. For example, accessing a std::vector with an invalid index using the .at() member function results in a std::out_of_range exception.

The <stdexcept> header defines several standard exception types for commonly occurring error conditions, including:

  • std::runtime_error: This is used for errors detected during program execution, such as calculation failures or missing resources.

  • std::invalid_argument: This is used when a function receives an argument that is invalid or logically incorrect, such as a negative age.

  • std::out_of_range: This is used when attempting to access an element outside the valid range of a container.

These exception types allow developers to attach descriptive error messages, which can be retrieved using the .what() member function, enabling clearer error reporting and easier debugging.

C++
#include <iostream>
#include <stdexcept> // Required for standard exceptions
#include <vector>
double precise_divide(double numerator, double denominator) {
if (denominator == 0) {
// Throw a standard exception with a descriptive message
throw std::invalid_argument("Cannot divide by zero.");
}
return numerator / denominator;
}
int main() {
try {
double result = precise_divide(10.0, 0.0);
std::cout << "Result: " << result << std::endl;
}
catch (const std::invalid_argument& e) {
// Catch specific errors by reference
std::cerr << "Caught invalid argument: " << e.what() << std::endl;
}
catch (const std::exception& e) {
// Catch any other standard exception (polymorphism)
std::cerr << "Caught standard exception: " << e.what() << std::endl;
}
return 0;
}

Let’s break this down step by step:

  • Line 13 (main starts): We enter the try block immediately.

  • Line 15: We call precise_divide(10.0, 0.0). Execution moves to line 5.

  • Lines 6–8 (inside precise_divide): The check denominator == 0 is true. We construct an std::invalid_argument object containing the string "Cannot divide by zero." and throw it. The function terminates immediately.

  • Lines 18–21 (first catch block): The execution stack unwinds to main. The program checks the first catch block: "Is the thrown object an std::invalid_argument?"

    • Yes: We enter this block. The exception object is bound to the reference e.

    • Line 20: We call e.what(), which returns our message "Cannot divide by zero.", and print it.

  • Line 22 (second catch block): Since the exception was already caught by the first block, this second block (for generic std::exception) is skipped.

  • Line 27: The program continues normally.

Exceptions vs. return codes

New developers often ask: "Why not just return -1 or false if something goes wrong?" Sometimes returning a value is appropriate, but exceptions are superior when:

  1. The error cannot be ignored: If a function returns -1 to signal error, the caller might ignore it and use -1 in a calculation. An exception forces the caller to handle it or crash.

  2. There is no valid "error value": If a function calculates a temperature, -1 might be a valid result (1 degree below zero). We can't use it to signal an error.

  3. The error is technically "exceptional": It shouldn't happen during normal operation (e.g., running out of memory or a hard drive failure).

To choose, the following points can be helpful:

  • Use exceptions for unexpected failures (e.g., "File corrupted", "Network unreachable").

  • Use return values (or checks) for expected conditions the program handles locally (e.g., "User entered strictly numeric password", "Search string not found").

The noexcept specifier

Sometimes we can guarantee that a function will never throw an exception. We can mark these functions with the noexcept keyword. This serves two purposes:

  1. Optimization: The compiler can generate faster code because it doesn't need to prepare for stack unwinding.

  2. Design intent: It tells other developers that this function is safe to call in critical sections.

C++ 23
#include <iostream>
// We promise this function will never throw
int safe_add(int a, int b) noexcept {
return a + b;
}
int main() {
std::cout << "Sum: " << safe_add(5, 3) << std::endl;
// We can check if a function is noexcept at compile-time
std::cout << "Is safe_add noexcept? " << noexcept(safe_add(1, 1)) << std::endl;
return 0;
}

Let’s break this down step by step:

  • Line 8 (main starts): Execution begins.

  • Line 9: We call safe_add(5, 3).

  • Line 4 (inside safe_add): The function is marked noexcept, signaling strictly that no throw will occur here.

  • Line 5: The addition happens, and the result (8) is returned normally.

  • Line 12: Back in main, we use the noexcept() operator. This is a compile-time check that returns true (or 1) because safe_add was declared with the noexcept specifier.

Note: If a function marked noexcept does throw an exception, the program will terminate immediately (crash) without unwinding the stack. Only use it when you are certain.

We now have a powerful way to make our programs resilient. Instead of letting errors propagate silently or cluttering our logic with endless checks, we can use try, catch, and standard exception types to handle failures cleanly. This separation of concerns, keeping error handling distinct from business logic, is a hallmark of professional C++ development.

In the next module, we will explore templates, which allow us to write generic code that works with any data type, a feature that relies heavily on the clean type safety we've been building up.