Skip to content

Modern C++ Error Handling

Published: at 01:34 PM

Modern C++ Error Handling

Table of contents

Open Table of contents

Part 1: Evolution of Error Handling in C++

Let’s examine different approaches using a simple example function that parses integers:

int parse_int(std::string_view number);

Exception Handling (Classic Approach)

Exceptions provide a way to separate error handling from regular code flow:

int parse_int(std::string_view number) {
	if (/* validation fails */)
		throw std::runtime_error(std::format("{} is not a number", number));
	// Parse and return number
}

void test() {
	try {
		int value = parse_int("abc");
		std::println("Value: {}", value);
	} 
	catch (std::exception& ex) {
		std::println("Error: {}", ex.what());
	}
}

C++17: std::optional

std::optional provides an elegant way to represent the possibility of a missing value:

std::optional<int> parse_int(std::string_view number) {
	if (/* validation fails */)
		return {};  // Return empty optional
	// Parse and return number
	return parsed_value;
}

void test(std::string_view str) {
	if (auto oi = parse_int(str)) 
		std::println("Value: {}", *oi);
	else
		std::println("Failed to parse number");
}

C++23: std::expected

std::expected is similar to Rust’s Result type, allowing you to return either a value or an error:

std::expected<int, std::runtime_error> parse_int(std::string_view number) {
	if (/* validation fails */)
		return std::unexpected(std::runtime_error("Invalid number format"));
	// Parse and return number
	return parsed_value;
}

void test(std::string_view str) {
	if (auto ei = parse_int(str)) 
		std::println("Value: {}", *ei);
	else
		std::println("Error: {}", ei.error().what());
}

C++23: Monadic Operations

C++23 introduces monadic operations for std::expected and std::optional, enabling functional-style error handling:

void test(std::string_view str) {
	auto ef = parse_int(str)
		.transform([](int i) {
			return i - 1;  // Transform the value
		})
		.and_then([](int i) -> std::expected<float, std::runtime_error> {
			if (i != 0)
				return 1.f / i;
			else
				return std::unexpected(std::runtime_error("divide by zero"));
		})
		.transform_error([](auto const& ex) {
			return std::string(ex.what());  // Transform the error
		});

	if (ef)
		std::println("Result: {}", *ef);
	else 
		std::println("Error: {}", ef.error());
}

Key Monadic Operations

Part 2: Design for Correctness

Sometimes the best error handling is preventing errors at compile-time or design-time.

Correct by Construction

Create types that can only represent valid states:

struct unchecked {};  // Tag type for unchecked construction

class Month {
	int month;
public:
	static bool is_valid(int month) {
		return month >= 1 && month <= 12;
	}
	
	// Safe constructor that validates input
	static std::optional<Month> create(int m) {
		if (is_valid(m))
			return Month(m, unchecked{});
		return {};
	}
	
	// Unchecked constructor (for internal use)
	Month(int month, unchecked): month(month) {
		assert(is_valid(month));
	}
	
	int value() const { return month; }
};

// Usage
std::string month_as_string(Month m) {
	static const std::array<std::string, 12> month_names = {
		"January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"
	};
	return month_names[m.value() - 1];
}

Contracts

C++ contracts provide a way to specify preconditions, postconditions, and invariants:

#ifndef NDEBUG
#define pre(expr) do {if(!(expr)) {violation_handler({#expr});}} while(false)
#else
#define pre(expr) do {} while(false)
#endif

struct ViolationInfo {
	std::string_view expr;
	std::source_location loc = std::source_location::current();
	
	std::string make_message() const {
		return std::format("Contract violation: {} at {}:{}", 
						  expr, loc.file_name(), loc.line());
	}
};

void default_violation_handler(ViolationInfo const& info) {
	std::print("{}", info.make_message());
	std::abort();
}

auto violation_handler = &default_violation_handler;

bool is_valid_month(int month) {
	return month >= 1 && month <= 12;
}

std::string month_as_string(int month) {
	pre(is_valid_month(month));
	static const std::array<std::string, 12> month_names = {
		"January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"
	};
	return month_names[month - 1];
}

Each approach has its strengths and ideal use cases. Modern C++ gives developers a rich set of tools for handling errors, enabling more expressive and safer code.


Next Post
Notes on CMake (part 1)