diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt
index 11f42ad642b408f2810a03bb7bc921f46c70c635..f980e8e1e929bd4055c09f27b9ee15733a8d86f1 100644
--- a/Test/CMakeLists.txt
+++ b/Test/CMakeLists.txt
@@ -12,6 +12,8 @@ set(my_headers
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumLevel.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumNumberT.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumOutcome.hh
+   ${CMAKE_CURRENT_SOURCE_DIR}/TestError.hh
+   ${CMAKE_CURRENT_SOURCE_DIR}/TestErrorInc.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestOutcome.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestPaths.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestReport.hh
@@ -30,6 +32,7 @@ set(my_sources
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumFile.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumIssueNumber.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumOutcome.cc
+   ${CMAKE_CURRENT_SOURCE_DIR}/TestError.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestOutcome.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestPaths.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestReport.cc
diff --git a/Test/TestArgs.cc b/Test/TestArgs.cc
index 04be492b4d47ba5cd0523c7cbd7816046335d937..b428e7885da580565d56a727e8bdeecaea71cc74 100755
--- a/Test/TestArgs.cc
+++ b/Test/TestArgs.cc
@@ -3,6 +3,7 @@
 #ifdef TEST_ON
 
 #include "TestArgs.hh"
+#include "TestError.hh"
 
 #include "Base/Test/TestChecksum.hh"
 #include "Base/Test/TestChecksumLevel.hh"
@@ -97,7 +98,7 @@ Option::Option(const std::string& _name, const int _min_arg_nmbr,
 uint Option::parse(const size_t _arg_nmbr, const std::string* const _args)
 {
   // Check that enough arguments have been supplied
-  OPTION_THROW_if(min_argument_number() > _arg_nmbr,
+  TEST_ARGS_THROW_if(min_argument_number() > _arg_nmbr,
       "Too few arguments supplied for "
           << name() << "." << Base::ENDL << INDENT << "expected: (>=)"
           << min_argument_number() << "; supplied: " << _arg_nmbr << "."
@@ -123,7 +124,7 @@ bool Option::check_and_set_mutex()
 
 void Option::throw_if_not_parsed() const
 {
-  OPTION_THROW_if(!parsed(),
+  TEST_ARGS_THROW_if(!parsed(),
       "The " << name()
              << " option is being accessed but it has not been parsed.");
 }
@@ -148,9 +149,10 @@ bool ToggleGroup::off() const
 
 void ToggleGroup::throw_if_not_parsed() const
 {
-  OPTION_THROW_if(!parsed(), "The '" << name_
-                                     << "' toggle option group is being "
-                                        "accessed but it has not been parsed.");
+  TEST_ARGS_THROW_if(!parsed(), "The '"
+                                    << name_
+                                    << "' toggle option group is being "
+                                       "accessed but it has not been parsed.");
 }
 
 ToggleGroup::ToggleGroup(const std::string& _name,
@@ -171,14 +173,14 @@ ToggleGroup::ToggleGroup(const std::string& _name,
 void Parser::add_option(Option& _option)
 {
   // Throw an error if the option attempts to override the -h or --help options.
-  OPTION_THROW_if(help_option(_option.name()),
+  TEST_ARGS_THROW_if(help_option(_option.name()),
       _option.name() << " cannot override -h or --help.");
 
 #ifdef __APPLE__
-  OPTION_THROW_if(!options_.emplace(_option.name(), _option).second,
+  TEST_ARGS_THROW_if(!options_.emplace(_option.name(), _option).second,
       _option.name() << " has already been added to the argument parser.");
-#else // __APPLE__
-  OPTION_THROW_if(!options_.try_emplace(_option.name(), _option).second,
+#else  // __APPLE__
+  TEST_ARGS_THROW_if(!options_.try_emplace(_option.name(), _option).second,
       _option.name() << " has already been added to the argument parser.");
 #endif // __APPLE__
 }
@@ -205,7 +207,7 @@ void Parser::parse()
     {
       // Throw error if the option is not recognised and the flag
       // FL_ALLOW_UNKNOWN is not on
-      OPTION_THROW_if(
+      TEST_ARGS_THROW_if(
           !flag_on<FL_ALLOW_UNKNOWN>(), args[i] << " is not a known option.");
       continue;
     }
@@ -214,12 +216,12 @@ void Parser::parse()
 
     // Throw error if option has already been set and the flag FL_OVERWRITE is
     // not on
-    OPTION_THROW_if(option.parsed() && !flag_on<FL_OVERWRITE>(),
+    TEST_ARGS_THROW_if(option.parsed() && !flag_on<FL_OVERWRITE>(),
         args[i] << " has already been set.");
 
     // Throw error if option is mutually exclusive with an option that has
     // already been set
-    OPTION_THROW_if(!option.check_and_set_mutex(),
+    TEST_ARGS_THROW_if(!option.check_and_set_mutex(),
         "An option that is mutually exclusive with "
             << args[i] << " has already been set.");
 
@@ -291,7 +293,7 @@ int DebugLevel::parse_level(const std::string& _arg)
   }
   catch (...)
   {
-    OPTION_THROW(name() << ": '" << _arg << "' is not a valid debug level.");
+    TEST_ARGS_THROW(name() << ": '" << _arg << "' is not a valid debug level.");
   }
 }
 
@@ -367,7 +369,8 @@ Checksum::Level ChecksumLevel::parse_level(const std::string& _arg)
       return static_cast<Checksum::Level>(i);
   }
 
-  OPTION_THROW(name() << ": '" << _arg << "' is not a valid checksum level.");
+  TEST_ARGS_THROW(
+      name() << ": '" << _arg << "' is not a valid checksum level.");
 }
 
 } // namespace Args
diff --git a/Test/TestArgs.hh b/Test/TestArgs.hh
index f15f1d2e0914505edb44e1a3fa7ee3531c786cf7..a8d0012114b54fbc6d8552bd55aab115f6688b21 100755
--- a/Test/TestArgs.hh
+++ b/Test/TestArgs.hh
@@ -54,21 +54,6 @@ environment variable. Internally, this calls collect_from_string().
 */
 void collect_from_variable(const char* const _vrbl_name);
 
-//! Throw a std::invalid_argument() exception
-#define OPTION_THROW(EXPR) \
-  { \
-    Base::OStringStream strm; \
-    strm << EXPR; \
-    throw std::invalid_argument(strm.str); \
-  }
-
-//! Throw a std::invalid_argument() exception is COND == true
-#define OPTION_THROW_if(COND, EXPR) \
-  { \
-    if (COND) \
-      OPTION_THROW(EXPR); \
-  }
-
 /*!
 Base class for test command-line options. Every option that we intend to parse
 must have an associated class derived from Option that implements the member
@@ -149,7 +134,7 @@ protected:
   /*!
   Virtual function to parse the arguments for the current option. The return
   value should be the number of arguments that have been successfully parsed.
-  Use OPTION_THROW[_if]() to report errors.
+  Use TEST_ARGS_THROW[_if]() (defined in TestError.hh) to report errors.
 
     * const size_t [_arg_nmbr]: Number of available arguments (guaranteed to be
       at least min_argument_number()).
diff --git a/Test/TestChecksumCompare.cc b/Test/TestChecksumCompare.cc
index 732cd8d57cb313c3844acacbcfe2258a6534d429..fae9147db9864399f81f71390758202281005978 100755
--- a/Test/TestChecksumCompare.cc
+++ b/Test/TestChecksumCompare.cc
@@ -6,12 +6,13 @@
 
 #include "TestChecksumCompare.hh"
 #include "TestChecksumCompletion.hh"
+#include "TestError.hh"
 
 #include "LongestCommonSubsequenceT.hh"
 
 #include <Base/Paths/Filesystem.hh>
-#include <Base/Utils/NullOutputStream.hh>
 #include <Base/Paths/PathLink.hh>
+#include <Base/Utils/NullOutputStream.hh>
 
 #include <exception>
 #include <fstream>
@@ -229,12 +230,8 @@ DifferenceDistribution Compare::Impl::run(Base::IOutputStream& _log_os) const
   const char* const RESULT_TAG  = "  ";
 
   // Throw an error if the report paths have not been set
-  if (left_path_.empty() || right_path_.empty())
-  {
-    throw std::runtime_error(
-        "Unable to compare checksum reports: Report paths have not been set. "
-        "This should be done using Test::Checksum::Compare::set_reports().");
-  }
+  TEST_THROW_ERROR_if(left_path_.empty() || right_path_.empty(),
+      TEST_CHECKSUM_COMPARE_REPORTS_NOT_SET);
 
   const Report rprts[2] = {Report(left_path_), Report(right_path_)};
 
@@ -318,12 +315,8 @@ void Compare::Impl::check_equal() const
   std::cout << "Info: Comparing checksums...";
 
   // Throw an error if the report paths have not been set
-  if (left_path_.empty() || right_path_.empty())
-  {
-    throw std::runtime_error(
-        "Unable to compare checksum reports: Report paths have not been set. "
-        "This should be done using Test::Checksum::Compare::set_reports().");
-  }
+  TEST_THROW_ERROR_if(left_path_.empty() || right_path_.empty(),
+      TEST_CHECKSUM_COMPARE_REPORTS_NOT_SET);
 
   // Skip the comparison if one of these files doesn't exist
   if (!fs::exists(left_path_))
@@ -359,7 +352,8 @@ void Compare::Impl::check_equal() const
 
     std::cout << "Compare " << PathLink(left_path_) << " with "
               << PathLink(right_path_) << "." << std::endl;
-    throw std::runtime_error("Checksum comparison failed--differences found!");
+
+    TEST_THROW_ERROR(TEST_CHECKSUM_COMPARE_DIFFERENCES);
   }
 }
 
@@ -376,17 +370,9 @@ void Compare::make()
 
 void Compare::set(Compare& _othr)
 {
-  if (_othr.impl_ != nullptr)
-  {
-    throw std::runtime_error(
-        "Unable to set implementation: incoming implementation is null.");
-  }
-
-  if (impl_ != nullptr)
-  {
-    throw std::runtime_error(
-        "Unable to set implementation: implementation has already been set.");
-  }
+  TEST_THROW_ERROR_if(
+      _othr.impl_ != nullptr, TEST_CHECKSUM_COMPARE_INCOMING_IMPL_NULL);
+  TEST_THROW_ERROR_if(impl_ != nullptr, TEST_CHECKSUM_COMPARE_IMPL_ALREADY_SET);
 
   impl_ = _othr.impl_;
 }
@@ -424,12 +410,7 @@ void Compare::check_equal() const
 
 void Compare::check_implementation() const
 {
-  if (impl_ == nullptr)
-  {
-    throw std::runtime_error("Implementation is null: Implementation should be "
-                             "set using Test::Checksum::Compare::make() or "
-                             "Test::Checksum::Compare::set().");
-  }
+  TEST_THROW_ERROR_if(impl_ == nullptr, TEST_CHECKSUM_COMPARE_IMPL_NULL);
 }
 
 Compare compare;
diff --git a/Test/TestError.cc b/Test/TestError.cc
new file mode 100755
index 0000000000000000000000000000000000000000..cbea590a4b4203e83c6278a69d5a3655d474e628
--- /dev/null
+++ b/Test/TestError.cc
@@ -0,0 +1,21 @@
+// (C) Copyright 2021 by Autodesk, Inc.
+
+#ifdef TEST_ON
+
+#include "TestError.hh"
+
+namespace Test
+{
+
+static const char* ERROR_MESSAGE[] =
+{
+  #define DEFINE_ERROR(CODE, MSG) MSG,
+  #include "TestErrorInc.hh"
+  #undef DEFINE_ERROR
+};
+
+const char* Error::message() const { return ERROR_MESSAGE[idx_]; }
+
+} // namespace Test
+
+#endif // TEST_ON
diff --git a/Test/TestError.hh b/Test/TestError.hh
new file mode 100755
index 0000000000000000000000000000000000000000..cd8b4586db3b35c7f1cd0072d658a39a94926ce3
--- /dev/null
+++ b/Test/TestError.hh
@@ -0,0 +1,70 @@
+// (C) Copyright 2021 by Autodesk, Inc.
+
+#ifndef BASE_TESTERROR_HH_INCLUDED
+#define BASE_TESTERROR_HH_INCLUDED
+
+#ifdef TEST_ON
+
+#include <Base/Utils/BaseError.hh>
+
+#include <exception>
+
+namespace Test
+{
+
+class BASEDLLEXPORT Error : public Base::Error
+{
+public:
+  enum Index
+  {
+  #define DEFINE_ERROR(CODE, MSG) CODE,
+  #include <Base/Test/TestErrorInc.hh>
+  #undef DEFINE_ERROR
+  };
+
+public:
+  //! Constructor.
+  Error(const Index _idx) : Base::Error((int)_idx) {}
+
+  //! Return the error message
+  virtual const char* message() const;
+
+protected:
+  Error(const int _idx) : Base::Error(_idx) {}
+};
+
+} // namespace Test
+
+// Throw a Test::Error exception with a specific error index (defined in
+// TestErrorInc.hh)
+#define TEST_THROW_ERROR(INDEX) { THROW_ERROR_MODULE(Test, INDEX); }
+#define TEST_THROW_ERROR_if(COND, INDEX) { if (COND) TEST_THROW_ERROR(INDEX); }
+
+// Throw a 'TODO' Test::Error exception and send a specific message to the debug
+// output
+#define TEST_THROW_ERROR_TODO(MSG) { THROW_ERROR_TODO_MODULE(Test, MSG); }
+#define TEST_THROW_ERROR_TODO_if(COND, MSG) { if (COND) TEST_THROW_ERROR_TODO(MSG); }
+
+// Throw a std::runtime_error exception with a message defined by EXPR
+#define TEST_THROW_MSG(EXPR) \
+  { \
+    Base::OStringStream strm; \
+    strm << EXPR; \
+    throw std::runtime_error(strm.str); \
+  }
+
+#define TEST_THROW_MSG_if(COND, EXPR) { if (COND) TEST_THROW_MSG(EXPR); }
+
+// Throw a std::invalid_argument exception with a message defined by EXPR
+#define TEST_ARGS_THROW(EXPR) \
+  { \
+    Base::OStringStream strm; \
+    strm << EXPR; \
+    throw std::invalid_argument(strm.str); \
+  }
+
+#define TEST_ARGS_THROW_if(COND, EXPR) { if (COND) TEST_ARGS_THROW(EXPR); }
+
+#endif // TEST_ON
+
+#endif // BASE_TESTERROR_HH_INCLUDED
diff --git a/Test/TestErrorInc.hh b/Test/TestErrorInc.hh
new file mode 100755
index 0000000000000000000000000000000000000000..aad554c284ca7a9103704ad46ce723b53700080b
--- /dev/null
+++ b/Test/TestErrorInc.hh
@@ -0,0 +1,32 @@
+// (C) Copyright 2021 by Autodesk, Inc.
+
+#ifndef DEFINE_ERROR
+#error This file should not be included directly, include TestError.hh instead
+#endif
+
+#ifdef TEST_ON
+
+#include <Base/Utils/BaseErrorInc.hh>
+
+// Error codes relating to TestChecksumCompare.(cc|hh)
+DEFINE_ERROR(TEST_CHECKSUM_COMPARE_IMPL_NULL,
+    "Implementation cannot be null. It should be set using "
+    "Test::Checksum::Compare::make() or Test::Checksum::Compare::set().")
+DEFINE_ERROR(TEST_CHECKSUM_COMPARE_REPORTS_NOT_SET,
+    "Report paths have not been set. This should be done using "
+    "Test::Checksum::Compare::set_reports().")
+DEFINE_ERROR(TEST_CHECKSUM_COMPARE_INCOMING_IMPL_NULL,
+    "Incoming implementation cannot be null.")
+DEFINE_ERROR(TEST_CHECKSUM_COMPARE_IMPL_ALREADY_SET,
+    "Implementation has already been set.")
+DEFINE_ERROR(TEST_CHECKSUM_COMPARE_DIFFERENCES,
+    "Checksum comparison failed: differences found!")
+
+// Error codes relating to TestOutcome.(cc|hh)
+DEFINE_ERROR(TEST_OUTCOME_UNEXPECTED, "Unexpected outcome.")
+
+// Error codes related to argument parsing
+DEFINE_ERROR(TEST_TOO_FEW_ARGS, "Too few arguments have been supplied.")
+DEFINE_ERROR(TEST_FAILED_CL_PARSE, "Failed to parse command-line arguments.")
+
+#endif // TEST_ON
diff --git a/Test/TestOutcome.cc b/Test/TestOutcome.cc
index decf45a64e155de6c42579ffdd5f616cae275523..41f2ee9c7d5a183fe63936b00e64c87e8fd1e10b 100755
--- a/Test/TestOutcome.cc
+++ b/Test/TestOutcome.cc
@@ -9,8 +9,8 @@
 #include "TestChecksum.hh"
 #include "TestChecksumCondition.hh"
 #include "TestChecksumOutcome.hh"
+#include "TestError.hh"
 
-#include <exception>
 #include <functional>
 #include <iostream>
 #include <string>
@@ -79,7 +79,7 @@ void expect_failure(const char* const _call, const Outcome& _oc)
 
 const Outcome::UnexpectedHandler Outcome::default_unexpected_handler = []
 {
-  throw std::runtime_error("Unexpected outcome");
+  TEST_THROW_ERROR(TEST_OUTCOME_UNEXPECTED);
 };
 
 } // namespace Test
diff --git a/Test/TestPaths.cc b/Test/TestPaths.cc
index 572463e65b2436ce44ab8a968964091d29694512..bb9b9adad5602ec7bdfcee4e53792ef719894622 100644
--- a/Test/TestPaths.cc
+++ b/Test/TestPaths.cc
@@ -4,6 +4,7 @@
 
 #include "TestPaths.hh"
 #include "TestChecksumNumberT.hh"
+#include "TestError.hh"
 
 #ifdef __APPLE__
 #include <boost/filesystem.hpp>
@@ -29,22 +30,6 @@ namespace fs = boost::filesystem;
 namespace fs = std::filesystem;
 #endif // __APPLE__
 
-// Similar to BASE_THROW[_if] but the errors are not expected to be converted in
-// error codes, so we just throw strings. Throwing exceptions through DLL
-// boundaries is not a great idea in general, but in this case it is OK as we
-// just want the test executable to fail immediately.
-// TODO: Should we move this stuff somewhere more visible?
-#define TEST_THROW(EXPR) \
-  { \
-    Base::OStringStream strm; \
-    strm << EXPR; \
-    throw std::runtime_error(strm.str); \
-  }
-
-#define TEST_THROW_if(COND, EXPR) \
-  if (COND) \
-  TEST_THROW(EXPR)
-
 Base::IOutputStream& operator<<(Base::IOutputStream& _os, const fs::path& _path)
 {
   return _os << _path.string();
@@ -65,7 +50,7 @@ void make_directory(const fs::path& _dir)
       // scheduled all together. In this case we can avoid a failure.
       // TODO: Make this even more secure, e.g., try this check a few more times
       // (5-10?), sleep the process before each attempt by 0.1sec.
-      TEST_THROW_if(!fs::exists(chk_dir),
+      TEST_THROW_MSG_if(!fs::exists(chk_dir),
           "Failed creating directory "
               << chk_dir << " in the requested directory path " << _dir);
     }
@@ -95,8 +80,11 @@ void Paths::init(const char* const _in_root, const char* const _in_rel_path,
   fs::path in_path = in_root / in_rel_path;
 
   // Verify arguments
-  TEST_THROW_if(!fs::is_directory(in_root), in_root << " is not a directory.");
-  TEST_THROW_if(!fs::is_regular_file(in_path), in_path << " is not a file.");
+  TEST_THROW_MSG_if(
+      !fs::is_directory(in_root), in_root << " is not a directory.");
+
+  TEST_THROW_MSG_if(
+      !fs::is_regular_file(in_path), in_path << " is not a file.");
 
   // Determine output directory (see comment in TestPaths.hh)
   auto out_dir = _out_root ? fs::path(_out_root) : fs::current_path();
@@ -135,7 +123,7 @@ Paths::Paths(
 #endif // TEST_WAIT_FOR_DEBUG_ATTACH
 
   // Check at least 2 arguments (i.e. test-bin arg) have been supplied.
-  TEST_THROW_if(_argc <= 1, "Test arguments have not been supplied.");
+  TEST_THROW_ERROR_if(_argc <= 1, TEST_TOO_FEW_ARGS);
 
   // Parse the first argument--this could be either the input root directory, or
   // a direct path to a test file
@@ -160,7 +148,7 @@ Paths::Paths(
   else
   {
     // Command-line arguments are not in the correct format.
-    TEST_THROW("Failed to parse command-line arguments.");
+    TEST_THROW_ERROR(TEST_FAILED_CL_PARSE);
   }
 }
 
diff --git a/Utils/BaseError.hh b/Utils/BaseError.hh
index 693d560398247ae9268002e26abc42f3922ecdd4..1258956d9b6f0dcb368b9e552cc9b98318e3e616 100644
--- a/Utils/BaseError.hh
+++ b/Utils/BaseError.hh
@@ -45,7 +45,7 @@ protected:
 }//namespace BASE
 
 #define BASE_THROW_ERROR(INDEX) { THROW_ERROR_MODULE(Base, INDEX); }
-#define BASE_THROW_ERROR_if(COND, INDEX) { if (COND) THROW_ERROR(INDEX); }
+#define BASE_THROW_ERROR_if(COND, INDEX) { if (COND) BASE_THROW_ERROR(INDEX); }
 
 #define BASE_THROW_ERROR_TODO(MSG) { THROW_ERROR_TODO_MODULE(Base, MSG); }
 #define BASE_THROW_ERROR_TODO_if(COND, MSG) { if (COND) BASE_THROW_ERROR_TODO(MSG); }
diff --git a/Utils/BaseErrorInc.hh b/Utils/BaseErrorInc.hh
index e86287995bbaa430855ca5a2a1f071f94f716618..3c2b7827e11d5de8031d3e8dae71ecfe3d0a5225 100644
--- a/Utils/BaseErrorInc.hh
+++ b/Utils/BaseErrorInc.hh
@@ -4,7 +4,7 @@
 #error This file should not be included directly, include the corresponding Outcome/Error header instead
 #endif
 
-// Make sure the first error code starts from 1, 0 is reserved for success, 
+// Make sure the first error code starts from 1, 0 is reserved for success,
 // and it is not an error code.
 DEFINE_ERROR(SUCCESS, "Success")
 DEFINE_ERROR(TODO, "TODO: Undefined error message")