This episode covers modern error handling: exceptions with throw and catch, stack unwinding, standard and custom exception types, debugging with GDB and sanitizers, as well as static analysis with clang-tidy and cppcheck.

Real-world programs will fail: files don't exist, networks drop, input is wrong. How you handle failure determines whether a program stays reliable or stops silently. C++ provides two approaches: C-style return codes and the more modern exception.
Episode 11 covers error handling thoroughly — throw, try, and catch, how stack unwinding works, standard exception types, creating custom exceptions, debugging with GDB and sanitizers, as well as static analysis with clang-tidy and cppcheck that catches bugs before the program even runs.
When an abnormal condition occurs, code throws an exception with throw. Potentially failing code is wrapped in try, and handling happens in catch. Execution jumps directly to the matching catch — code in between is skipped:
cat > exc.cpp <<'EOF'
#include <iostream>
#include <stdexcept>
double bagi(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("pembagian dengan nol");
}
return a / b;
}
int main() {
try {
std::cout << bagi(10, 0) << "\n";
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
}
}
EOF
g++ -std=c++20 exc.cpp -o exc
./excthrow std::runtime_error("pembagian dengan nol") throws an exception with a message, and catch (const std::exception& e) catches it. e.what() returns the error message. Multiple catch blocks handle different types; catch the more specific one before the more general one. Always throw exceptions by value and catch by const reference — throwing pointers carries no type information and is leak-prone.
When an exception is thrown, all objects on the stack between the throw point and the catch point are destroyed — this is called stack unwinding. Each object's destructor is still called, so resources held by RAII are released correctly. This is why the RAII pattern from episode 7 matters so much: even when an exception occurs, there are no leaks.
<stdexcept> provides a ready-made exception hierarchy, all derived from std::exception: std::runtime_error for runtime errors, std::logic_error for logic violations, std::invalid_argument for invalid arguments, std::out_of_range for out-of-bounds access, and std::bad_alloc, thrown when a memory allocation fails. Catch the specific ones first:
cat > multi.cpp <<'EOF'
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3};
try {
std::cout << v.at(10) << "\n";
} catch (const std::out_of_range& e) {
std::cerr << "Di luar batas: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << "Error lain: " << e.what() << "\n";
}
}
EOF
g++ -std=c++20 multi.cpp -o multi
./multiv.at(10) throws std::out_of_range when the index exceeds the size — unlike v[10], which doesn't check bounds. Note the order: out_of_range is caught before the general std::exception.
For domain-specific errors, create your own exception by deriving from std::exception and overriding what():
cat > custom.cpp <<'EOF'
#include <iostream>
#include <exception>
#include <string>
class SaldoTidakCukup : public std::exception {
public:
explicit SaldoTidakCukup(double saldo)
: pesan_("Saldo tidak cukup, tersedia " +
std::to_string(saldo)) {}
const char* what() const noexcept override {
return pesan_.c_str();
}
private:
std::string pesan_;
};
int main() {
try {
throw SaldoTidakCukup(10.5);
} catch (const SaldoTidakCukup& e) {
std::cerr << e.what() << "\n";
}
}
EOF
g++ -std=c++20 custom.cpp -o custom
./customSaldoTidakCukup(10.5) carries context data — the balance — and builds an informative message. what() is marked noexcept per the standard contract.
GDB is the command-line debugger for Linux. You can set breakpoints, step through lines, and inspect variable values. Build with -g so debug symbols are stored:
cat > bug.cpp <<'EOF'
#include <iostream>
int main() {
int total = 0;
for (int i = 1; i <= 5; ++i) {
total += i;
}
std::cout << "Total: " << total << "\n";
}
EOF
g++ -g -std=c++20 bug.cpp -o bug
gdb -batch -ex 'break main' -ex 'run' -ex 'next' -ex 'print total' ./bugThe command gdb -batch -ex 'break main' -ex 'run' -ex 'print total' ./bug runs GDB without interactive mode: stops at main, runs, then prints the total variable. The -g flag during compilation stores debug information.
AddressSanitizer detects use-after-free, buffer overflows, and memory leaks. Enable it with a compiler flag, then run as usual:
g++ -fsanitize=address -g -std=c++20 cek.cpp -o cek
./cek-fsanitize=address installs runtime memory checks. If the program touches invalid memory, ASan reports the exact location with a stack trace. It's the most effective tool for chasing memory bugs before going to production.
Static analysis inspects code without running it. clang-tidy gives style warnings and potential bugs, while cppcheck detects issues like use-after-return and memory leaks. Both are an important part of a quality pipeline:
clang-tidy bug.cpp -- -std=c++20
cppcheck --enable=all --std=c++20 bug.cppThe command clang-tidy bug.cpp -- -std=c++20 analyzes the file with the same compilation options. cppcheck --enable=all enables all checks. Get in the habit of running both in CI — episode 19 will automate this.
Tip
-Wall -Wextra for compiler warnings, sanitizers for runtime errors, and clang-tidy or cppcheck for static analysis.Here's what to take away:
throw throws an exception, try and catch handle it.std::exception.std::exception and override what().-g for GDB and -fsanitize=address for memory bug detection.In the next episode, episode 12, we'll discuss concurrency and multithreading — std::thread, std::mutex, and std::lock_guard, condition variables, std::future and std::promise along with std::async, thread safety with data races and deadlocks, as well as building thread pools for parallel workloads.