Skip to content

feat(error_handling): Migrate TraceableException from CLP with changes: #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 8, 2025
3 changes: 3 additions & 0 deletions src/ystdlib/error_handling/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -3,9 +3,12 @@ cpp_library(
NAMESPACE ystdlib
PUBLIC_HEADERS
ErrorCode.hpp
TraceableException.hpp
utils.hpp
TESTS_SOURCES
test/constants.hpp
test/test_ErrorCode.cpp
test/test_TraceableException.cpp
test/types.cpp
test/types.hpp
)
78 changes: 78 additions & 0 deletions src/ystdlib/error_handling/TraceableException.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#ifndef YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
#define YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP

#include <exception>
#include <source_location>
#include <sstream>
#include <string>
#include <system_error>
#include <utility>

#include "utils.hpp"

namespace ystdlib::error_handling {
/**
* An exception class that is thrown with an `std::error_code`.
*
* This class extends `std::exception` and can be thrown with an `std::error_code` argument. It also
* provides additional information to aid in debugging by storing details in `std::source_location`,
* including the function name, file name, and line number of the throwing location.
*
* @see std::source_location::file_name()
* @see std::source_location::function_name()
* @see std::source_location::line()
*/
class TraceableException : public std::exception {
public:
// Constructors
explicit TraceableException(
std::error_code error_code,
std::source_location const& where = std::source_location::current()
)
: m_error_code{error_code},
m_where{where} {
std::ostringstream oss;
oss << where;
m_what = oss.str();
}

explicit TraceableException(
std::error_code error_code,
std::string message,
std::source_location const& where = std::source_location::current()
)
: m_error_code{error_code},
m_what{std::move(message)},
m_where{where} {}

// Methods implementing std::exception
[[nodiscard]] auto what() const noexcept -> char const* override { return m_what.c_str(); }

// Methods
[[nodiscard]] auto error_code() const -> std::error_code { return m_error_code; }

[[nodiscard]] auto what() -> std::string& { return m_what; }

[[nodiscard]] auto where() const noexcept -> std::source_location const& { return m_where; }
Comment on lines +49 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid overloading 'what' with differing return types.
Having both what() const noexcept -> char const* (line 49) and what() -> std::string& (line 54) can cause confusion. Callers might expect the standard signature only. If read–write access to m_what is needed, consider offering a different named method.


private:
// Variables
std::error_code m_error_code;
std::string m_what;
std::source_location m_where;
};
} // namespace ystdlib::error_handling

// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
/**
* Defines a derived `TraceableException` class with the given class name.
*
* @param T The class' name.
*/
#define YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(T) \
class T : public ystdlib::error_handling::TraceableException { \
using ystdlib::error_handling::TraceableException::TraceableException; \
}
// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)

#endif // YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
67 changes: 67 additions & 0 deletions src/ystdlib/error_handling/test/test_TraceableException.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <cassert>
#include <cstring>

#include <ystdlib/error_handling/TraceableException.hpp>

#include <catch2/catch_test_macros.hpp>

#include "types.hpp"

using ystdlib::error_handling::TraceableException;

namespace {
constexpr auto cCustomFailureDescription{"This operation failed due to invalid args."};
constexpr auto cCurrentFileName{"src/ystdlib/error_handling/test/test_TraceableException.cpp"};
constexpr auto cSuccessFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_success()"
};
constexpr auto cFailureFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_failure()"
};
} // namespace

namespace ystdlib::error_handling::test {
class Worker {
public:
YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(OperationFailed);

static auto execute_with_success() -> void {
throw OperationFailed(BinaryErrorCode{BinaryErrorCodeEnum::Success});
}

static auto execute_with_failure() -> void {
throw OperationFailed(
BinaryErrorCode{BinaryErrorCodeEnum::Failure},
cCustomFailureDescription
);
}
};
} // namespace ystdlib::error_handling::test

namespace {
template <typename Callable>
[[nodiscard]] auto capture_exception(Callable&& f) -> TraceableException;

template <typename Callable>
auto capture_exception(Callable&& f) -> TraceableException {
try {
std::forward<Callable>(f)();
} catch (TraceableException& e) {
return e;
}
assert(false && "The function is expected to throw.");
}
Comment on lines +41 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety with template constraints.
The capture_exception template returns a TraceableException and expects that exact type to be thrown. If a different type of exception is actually thrown, the code falls through to assert(false). Consider constraining your template to accept callables that are guaranteed to throw TraceableException or handle unexpected exception types more gracefully.

} // namespace

namespace ystdlib::error_handling::test {
TEST_CASE("test_traceable_exception", "[error_handling][TraceableException]") {
auto const ex_success{capture_exception(Worker::execute_with_success)};
REQUIRE(std::string{ex_success.where().file_name()}.ends_with(cCurrentFileName));
REQUIRE((0 == std::strcmp(ex_success.where().function_name(), cSuccessFuncName)));

auto const ex_failure{capture_exception(Worker::execute_with_failure)};
REQUIRE((0 == std::strcmp(ex_failure.what(), cCustomFailureDescription)));
REQUIRE(std::string{ex_failure.where().file_name()}.ends_with(cCurrentFileName));
REQUIRE((0 == std::strcmp(ex_failure.where().function_name(), cFailureFuncName)));
}
} // namespace ystdlib::error_handling::test
20 changes: 20 additions & 0 deletions src/ystdlib/error_handling/utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef YSTDLIB_ERROR_HANDLING_UTILS_HPP
#define YSTDLIB_ERROR_HANDLING_UTILS_HPP

#include <ostream>
#include <source_location>

namespace ystdlib::error_handling {
/**
* Writes a formatted representation of `std::source_location` to the output stream.
*
* @param os
* @param where
*/
inline auto operator<<(std::ostream& os, std::source_location const& where) -> std::ostream& {
return os << where.file_name() << "(" << where.line() << ":" << where.column()
<< "), function `" << where.function_name() << "`";
}
} // namespace ystdlib::error_handling

#endif // YSTDLIB_ERROR_HANDLING_UTILS_HPP