diff --git a/automation/tests/aegisub.cpp b/automation/tests/aegisub.cpp
index 006f15b6f637ab9cb7a69b4d45e71494ad6fbf31..b860901ac19ae6dce4c39676271cf9fb68413a47 100644
--- a/automation/tests/aegisub.cpp
+++ b/automation/tests/aegisub.cpp
@@ -57,7 +57,7 @@ int main(int argc, char **argv) {
 	try {
 		check(L, !LoadFile(L, argv[1]));
 	} catch (agi::Exception const& e) {
-		fprintf(stderr, "%s\n", e.GetChainedMessage().c_str());
+		fprintf(stderr, "%s\n", e.GetMessage().c_str());
 	}
 
 	for (int i = 2; i < argc; ++i)
diff --git a/libaegisub/common/character_count.cpp b/libaegisub/common/character_count.cpp
index 07ebf5fcc6b2e5b03dc9dcc3cc744d84a664499d..0b8c70b315a8f0d65b83dc56b8686d0575de8f69 100644
--- a/libaegisub/common/character_count.cpp
+++ b/libaegisub/common/character_count.cpp
@@ -37,15 +37,15 @@ icu::BreakIterator& get_break_iterator(const char *ptr, size_t len) {
 	std::call_once(token, [&] {
 		UErrorCode status = U_ZERO_ERROR;
 		bi.reset(BreakIterator::createCharacterInstance(Locale::getDefault(), status));
-		if (U_FAILURE(status)) throw agi::InternalError("Failed to create character iterator", nullptr);
+		if (U_FAILURE(status)) throw agi::InternalError("Failed to create character iterator");
 	});
 
 	UErrorCode err = U_ZERO_ERROR;
 	utext_ptr ut(utext_openUTF8(nullptr, ptr, len, &err));
-	if (U_FAILURE(err)) throw agi::InternalError("Failed to open utext", nullptr);
+	if (U_FAILURE(err)) throw agi::InternalError("Failed to open utext");
 
 	bi->setText(ut.get(), err);
-	if (U_FAILURE(err)) throw agi::InternalError("Failed to set break iterator text", nullptr);
+	if (U_FAILURE(err)) throw agi::InternalError("Failed to set break iterator text");
 
 	return *bi;
 }
diff --git a/libaegisub/common/file_mapping.cpp b/libaegisub/common/file_mapping.cpp
index e12ed1106fdf79c3a2d9265bbc9018423a710624..5102694963c8fbb3628e00ad3bfcd641e4bc2a3d 100644
--- a/libaegisub/common/file_mapping.cpp
+++ b/libaegisub/common/file_mapping.cpp
@@ -40,7 +40,7 @@ char *map(int64_t s_offset, uint64_t length, boost::interprocess::mode_t mode,
 
 	auto offset = static_cast<uint64_t>(s_offset);
 	if (offset + length > file_size)
-		throw agi::InternalError("Attempted to map beyond end of file", nullptr);
+		throw agi::InternalError("Attempted to map beyond end of file");
 
 	// Check if we can just use the current mapping
 	if (region && offset >= mapping_start && offset + length <= mapping_start + region->get_size())
@@ -150,9 +150,9 @@ temp_file_mapping::temp_file_mapping(fs::path const& filename, uint64_t size)
 	unlink(filename.string().c_str());
 	if (ftruncate(handle, size) == -1) {
 		switch (errno) {
-		case EBADF:  throw InternalError("Error opening file " + filename.string() + " not handled", nullptr);
+		case EBADF:  throw InternalError("Error opening file " + filename.string() + " not handled");
 		case EFBIG:  throw fs::DriveFull(filename);
-		case EINVAL: throw InternalError("File opened incorrectly: " + filename.string(), nullptr);
+		case EINVAL: throw InternalError("File opened incorrectly: " + filename.string());
 		case EROFS:  throw fs::WriteDenied(filename);
 		default: throw fs::FileSystemUnknownError("Unknown error opening file: " + filename.string());
 		}
diff --git a/libaegisub/common/option_visit.cpp b/libaegisub/common/option_visit.cpp
index 12716394d00d2936967bb75778780cfe1b0c8b5c..7c2f5bacc5f9d340c9f413c0f55d55ebac90ce6a 100644
--- a/libaegisub/common/option_visit.cpp
+++ b/libaegisub/common/option_visit.cpp
@@ -150,7 +150,7 @@ void ConfigVisitor::AddOptionValue(std::unique_ptr<OptionValue>&& opt) {
 		}
 		catch (agi::OptionValueError const& e) {
 			if (ignore_errors)
-				LOG_E("option/load/config_visitor") << "Error loading option from user configuration: " << e.GetChainedMessage();
+				LOG_E("option/load/config_visitor") << "Error loading option from user configuration: " << e.GetMessage();
 			else
 				throw;
 		}
diff --git a/libaegisub/common/option_visit.h b/libaegisub/common/option_visit.h
index a53bf3bd0789839adf9da4005bf3dd92387b5a10..529db404a558a3492a9e2da39fe4c56e732f9a43 100644
--- a/libaegisub/common/option_visit.h
+++ b/libaegisub/common/option_visit.h
@@ -25,10 +25,10 @@
 
 namespace agi {
 
-DEFINE_BASE_EXCEPTION_NOINNER(OptionJsonValueError, Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionJsonValueArray, OptionJsonValueError, "options/value/array")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionJsonValueSingle, OptionJsonValueError, "options/value")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionJsonValueNull, OptionJsonValueError, "options/value")
+DEFINE_EXCEPTION(OptionJsonValueError, Exception);
+DEFINE_EXCEPTION(OptionJsonValueArray, OptionJsonValueError);
+DEFINE_EXCEPTION(OptionJsonValueSingle, OptionJsonValueError);
+DEFINE_EXCEPTION(OptionJsonValueNull, OptionJsonValueError);
 
 class ConfigVisitor final : public json::ConstVisitor {
 	/// Option map being populated
diff --git a/libaegisub/common/path.cpp b/libaegisub/common/path.cpp
index b3a171fc98ec1efe7a3050e9159541acba59ee67..7fa7e899d3be7f5e14939798455283f808f0f489 100644
--- a/libaegisub/common/path.cpp
+++ b/libaegisub/common/path.cpp
@@ -62,7 +62,7 @@ fs::path Path::Decode(std::string const& path) const {
 
 fs::path Path::MakeRelative(fs::path const& path, std::string const& token) const {
 	const auto it = tokens.find(token);
-	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token, nullptr);
+	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token);
 
 	return MakeRelative(path, it->second);
 }
@@ -94,7 +94,7 @@ fs::path Path::MakeRelative(fs::path const& path, fs::path const& base) const {
 fs::path Path::MakeAbsolute(fs::path path, std::string const& token) const {
 	if (path.empty()) return path;
 	const auto it = tokens.find(token);
-	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token, nullptr);
+	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token);
 
 	path.make_preferred();
 	const auto str = path.string();
@@ -123,7 +123,7 @@ std::string Path::Encode(fs::path const& path) const {
 
 void Path::SetToken(std::string const& token_name, fs::path const& token_value) {
 	const auto it = tokens.find(token_name);
-	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token_name, nullptr);
+	if (it == tokens.end()) throw agi::InternalError("Bad token: " + token_name);
 
 	if (token_value.empty())
 		it->second = token_value;
diff --git a/libaegisub/include/libaegisub/charset.h b/libaegisub/include/libaegisub/charset.h
index 0c6cdbd172f011349939a4b4e542fdcb0501e628..4851c2f261a69d1aa975331131a6bdf0dcf9e589 100644
--- a/libaegisub/include/libaegisub/charset.h
+++ b/libaegisub/include/libaegisub/charset.h
@@ -26,8 +26,8 @@ namespace agi {
 	/// Character set conversion and detection.
 	namespace charset {
 
-DEFINE_BASE_EXCEPTION_NOINNER(CharsetError, agi::Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(UnknownCharset, CharsetError, "charset/unknown")
+DEFINE_EXCEPTION(CharsetError, agi::Exception);
+DEFINE_EXCEPTION(UnknownCharset, CharsetError);
 
 /// List of detected encodings.
 typedef std::vector<std::pair<float, std::string>> CharsetListDetected;
diff --git a/libaegisub/include/libaegisub/charset_conv.h b/libaegisub/include/libaegisub/charset_conv.h
index be0fdcfa6bfb3bae63e33cd416cd46d7c641a4b5..057d4ed75fa62f02b084ced5c4cab3ad4b143182 100644
--- a/libaegisub/include/libaegisub/charset_conv.h
+++ b/libaegisub/include/libaegisub/charset_conv.h
@@ -27,12 +27,12 @@
 namespace agi {
 	namespace charset {
 
-DEFINE_BASE_EXCEPTION_NOINNER(ConvError, Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(UnsupportedConversion, ConvError, "iconv/unsupported")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(ConversionFailure, ConvError, "iconv/failed")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(BufferTooSmall, ConversionFailure, "iconv/failed/E2BIG")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(BadInput, ConversionFailure, "iconv/failed/EILSEQ")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(BadOutput, ConversionFailure, "iconv/failed/EINVAL")
+DEFINE_EXCEPTION(ConvError, Exception);
+DEFINE_EXCEPTION(UnsupportedConversion, ConvError);
+DEFINE_EXCEPTION(ConversionFailure, ConvError);
+DEFINE_EXCEPTION(BufferTooSmall, ConversionFailure);
+DEFINE_EXCEPTION(BadInput, ConversionFailure);
+DEFINE_EXCEPTION(BadOutput, ConversionFailure);
 
 typedef void* iconv_t;
 
diff --git a/libaegisub/include/libaegisub/exception.h b/libaegisub/include/libaegisub/exception.h
index a4094910de4ce40d8bccf261a45f4d6f68c9fb92..3b9411db6cb5823e1cdc8e0c4db1bae66c22dc36 100644
--- a/libaegisub/include/libaegisub/exception.h
+++ b/libaegisub/include/libaegisub/exception.h
@@ -27,14 +27,8 @@
 //
 // Aegisub Project http://www.aegisub.org/
 
-/// @file exception.h
-/// @brief Base exception classes for structured error handling
-/// @ingroup main_headers
-///
-
 #pragma once
 
-#include <memory>
 #include <string>
 
 namespace agi {
@@ -90,26 +84,13 @@ namespace agi {
 		/// The error message
 		std::string message;
 
-		/// An inner exception, the cause of this exception
-		std::shared_ptr<Exception> inner;
-
 	protected:
 		/// @brief Protected constructor initialising members
 		/// @param msg The error message
-		/// @param inr The inner exception, optional
 		///
 		/// Deriving classes should always use this constructor for initialising
 		/// the base class.
-		Exception(std::string msg, const Exception *inr = nullptr) : message(std::move(msg)) {
-			if (inr)
-				inner.reset(inr->Copy());
-		}
-
-		/// @brief Default constructor, not implemented
-		///
-		/// The default constructor is not implemented because it must not be used,
-		/// as it leaves the members un-initialised.
-		Exception();
+		Exception(std::string msg) : message(std::move(msg)) { }
 
 	public:
 		/// @brief Destructor
@@ -117,41 +98,7 @@ namespace agi {
 
 		/// @brief Get the outer exception error message
 		/// @return Error message
-		virtual std::string GetMessage() const { return message; }
-
-		/// @brief Get error messages for chained exceptions
-		/// @return Chained error message
-		///
-		/// If there is an inner exception, prepend its chained error message to
-		/// our error message, with a CRLF between. Returns our own error message
-		/// alone if there is no inner exception.
-		std::string GetChainedMessage() const { if (inner.get()) return inner->GetChainedMessage() + "\r\n" + GetMessage(); else return GetMessage(); }
-		
-		/// @brief Exception class printable name
-		///
-		/// Sub classes should implement this to return a constant character string
-		/// naming their exception in a hierarchic manner.
-		///
-		/// Exception classes inheriting directly from Exception define a top-level
-		/// name for their sub-tree, further sub-classes add further levels, each
-		/// level is separated by a slash. Characters allowed in the name for a
-		/// level are [a-z0-9_].
-		virtual const char * GetName() const = 0;
-
-		/// @brief Convert to char array as the error message
-		/// @return The error message
-		operator const char * () { return GetMessage().c_str(); }
-
-		/// @brief Convert to std::string as the error message
-		/// @return The error message
-		operator std::string () { return GetMessage(); }
-
-		/// @brief Create a copy of the exception allocated on the heap
-		/// @return A heap-allocated exception object
-		///
-		/// All deriving classes must implement this explicitly to avoid losing
-		/// information in the duplication.
-		virtual Exception *Copy() const = 0;
+		std::string const& GetMessage() const { return message; }
 	};
 
 /// @brief Convenience macro to include the current location in code
@@ -160,62 +107,14 @@ namespace agi {
 /// indicate the exact position the error occurred at.
 #define AG_WHERE " (at " __FILE__ ":" #__LINE__ ")"
 
-/// @brief Convenience macro for declaring exceptions with no support for inner exception
+/// @brief Convenience macro for declaring exceptions
 /// @param classname   Name of the exception class to declare
 /// @param baseclass   Class to derive from
-/// @param displayname The printable name of the exception (return of GetName())
-///
-/// This macro covers most cases of exception classes where support for inner
-/// exceptions is not relevant/wanted.
-#define DEFINE_SIMPLE_EXCEPTION_NOINNER(classname,baseclass,displayname)             \
-	class classname : public baseclass {                                             \
-	public:                                                                          \
-		classname(const std::string &msg) : baseclass(msg) { }                          \
-		const char * GetName() const { return displayname; }                   \
-		Exception * Copy() const { return new classname(*this); }                    \
-	};
-
-/// @brief Convenience macro for declaring exceptions supporting inner exceptions
-/// @param classname   Name of the exception class to declare
-/// @param baseclass   Class to derive from
-/// @param displayname The printable name of the exception (return of GetName())
-///
-/// This macro covers most cases of exception classes that should support
-/// inner exceptions.
-#define DEFINE_SIMPLE_EXCEPTION(classname,baseclass,displayname)                     \
-	class classname : public baseclass {                                             \
-	public:                                                                          \
-		classname(const std::string &msg, Exception *inner) : baseclass(msg, inner) { } \
-		const char * GetName() const { return displayname; }                   \
-		Exception * Copy() const { return new classname(*this); }                    \
-	};
-
-/// @brief Macro for declaring non-instantiable exception base classes
-/// @param classname Name of the exception class to declare
-/// @param baseclass Class to derive from
-///
-/// Declares an exception class that does not implement the GetName() function
-/// and as such (unless a base class implements it) is not constructable.
-/// Classes declared by this macro do not support inner exceptions.
-#define DEFINE_BASE_EXCEPTION_NOINNER(classname,baseclass)                           \
-	class classname : public baseclass {                                             \
-	public:                                                                          \
-		classname(const std::string &msg) : baseclass(msg) { }                          \
-	};
-
-/// @brief Macro for declaring non-instantiable exception base classes with inner
-///        exception support
-/// @param classname Name of the exception class to declare
-/// @param baseclass Class to derive from
-///
-/// Declares an exception class that does not implement the GetName() function
-/// and as such (unless a base class implements it) is not constructable.
-/// Classes declared by this macro do support inner exceptions.
-#define DEFINE_BASE_EXCEPTION(classname,baseclass)                                   \
-	class classname : public baseclass {                                             \
-	public:                                                                          \
-		classname(const std::string &msg, Exception *inner) : baseclass(msg, inner) { } \
-	};
+#define DEFINE_EXCEPTION(classname, baseclass)                 \
+class classname : public baseclass {                           \
+public:                                                        \
+	classname(std::string msg) : baseclass(std::move(msg)) { } \
+}
 
 	/// @class agi::UserCancelException
 	/// @extends agi::Exception
@@ -229,7 +128,7 @@ namespace agi {
 	/// possible, user cancel exceptions should unwind anything that was going on at the
 	/// moment. For this to work, RAII methodology has to be used consequently in the
 	/// code in question.
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(UserCancelException,Exception,"nonerror/user_cancel")
+	DEFINE_EXCEPTION(UserCancelException, Exception);
 
 	/// @class agi::InternalError
 	/// @extends agi:Exception
@@ -240,7 +139,7 @@ namespace agi {
 	/// have become inconsistent. All internal errors are of the type "this should never
 	/// happen", most often you'll want this kind of error unwind all the way past the main UI
 	/// and eventually cause an abort().
-	DEFINE_SIMPLE_EXCEPTION(InternalError, Exception, "internal_error")
+	DEFINE_EXCEPTION(InternalError, Exception);
 
 	/// @class agi::EnvironmentError
 	/// @extends agi:Exception
@@ -249,10 +148,10 @@ namespace agi {
 	/// Throw an environment error when a call to the platform API has failed
 	/// in some way that should normally never happen or suggests that the
 	/// runtime environment is too insane to support.
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(EnvironmentError, Exception, "environment_error")
+	DEFINE_EXCEPTION(EnvironmentError, Exception);
 
 	/// @class agi::InvalidInputException
 	/// @extends agi::Exception
 	/// @brief Some input data were invalid and could not be processed
-	DEFINE_BASE_EXCEPTION(InvalidInputException, Exception)
+	DEFINE_EXCEPTION(InvalidInputException, Exception);
 }
diff --git a/libaegisub/include/libaegisub/fs.h b/libaegisub/include/libaegisub/fs.h
index c1516e971ac5a116589f874c920b7de38924d7d1..6dbeaf81bc2e183b4b1285daeb099f3bd75d98be 100644
--- a/libaegisub/include/libaegisub/fs.h
+++ b/libaegisub/include/libaegisub/fs.h
@@ -44,7 +44,7 @@ namespace agi {
 		/// This base class can not be instantiated.
 		/// File system errors do not support inner exceptions, as they
 		/// are always originating causes for errors.
-		DEFINE_BASE_EXCEPTION_NOINNER(FileSystemError, Exception)
+		DEFINE_EXCEPTION(FileSystemError, Exception);
 
 		/// A file can't be accessed for some reason
 		DEFINE_FS_EXCEPTION(FileNotAccessible, FileSystemError, "File is not accessible: ");
@@ -53,7 +53,7 @@ namespace agi {
 		DEFINE_FS_EXCEPTION(FileNotFound, FileNotAccessible, "File not found: ");
 
 		/// An error of some unknown type has occured
-		DEFINE_SIMPLE_EXCEPTION_NOINNER(FileSystemUnknownError, FileSystemError, "filesystem/unknown");
+		DEFINE_EXCEPTION(FileSystemUnknownError, FileSystemError);;
 
 		/// The path exists, but isn't a file
 		DEFINE_FS_EXCEPTION(NotAFile, FileNotAccessible, "Path is not a file (and should be): ");
diff --git a/libaegisub/include/libaegisub/io.h b/libaegisub/include/libaegisub/io.h
index 4142aa4dba48d0f7186788c0d7b139d680d36fdc..beb5ba06d08f2541600e1de5347136aaeb0d51a4 100644
--- a/libaegisub/include/libaegisub/io.h
+++ b/libaegisub/include/libaegisub/io.h
@@ -26,8 +26,8 @@
 namespace agi {
 	namespace io {
 
-DEFINE_BASE_EXCEPTION_NOINNER(IOError, Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(IOFatal, IOError, "io/fatal")
+DEFINE_EXCEPTION(IOError, Exception);
+DEFINE_EXCEPTION(IOFatal, IOError);
 
 std::unique_ptr<std::istream> Open(fs::path const& file, bool binary = false);
 
diff --git a/libaegisub/include/libaegisub/keyframe.h b/libaegisub/include/libaegisub/keyframe.h
index 8f0eb459f1189408ceffea2fbe3380d34b27891a..43fb9333f46dfd705ddb7ecff605767194972308 100644
--- a/libaegisub/include/libaegisub/keyframe.h
+++ b/libaegisub/include/libaegisub/keyframe.h
@@ -12,10 +12,10 @@
 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-#include <vector>
+#include <libaegisub/exception.h>
+#include <libaegisub/fs_fwd.h>
 
-#include "exception.h"
-#include "fs_fwd.h"
+#include <vector>
 
 namespace agi {
 	namespace keyframe {
@@ -29,6 +29,6 @@ namespace agi {
 		/// @param keyframes List of keyframes to save
 		void Save(agi::fs::path const& filename, std::vector<int> const& keyframes);
 
-		DEFINE_SIMPLE_EXCEPTION_NOINNER(Error, Exception, "keyframe/error")
+		DEFINE_EXCEPTION(Error, Exception);
 	}
 }
diff --git a/libaegisub/include/libaegisub/lua/utils.h b/libaegisub/include/libaegisub/lua/utils.h
index 6fba99dda3dde09aa14c3e87ba4d771e4e14d2fc..4e907eea9505ee56cf2605cb9451e136c17e4ced 100644
--- a/libaegisub/include/libaegisub/lua/utils.h
+++ b/libaegisub/include/libaegisub/lua/utils.h
@@ -79,7 +79,7 @@ int exception_wrapper(lua_State *L) {
 		return func(L);
 	}
 	catch (agi::Exception const& e) {
-		push_value(L, e.GetChainedMessage());
+		push_value(L, e.GetMessage());
 		return lua_error(L);
 	}
 	catch (std::exception const& e) {
diff --git a/libaegisub/include/libaegisub/mru.h b/libaegisub/include/libaegisub/mru.h
index e722a022ff53242d8475f620082ba8718c211836..a2b24bd106139a6619a709efb9ad29bc4294d2a5 100644
--- a/libaegisub/include/libaegisub/mru.h
+++ b/libaegisub/include/libaegisub/mru.h
@@ -27,9 +27,9 @@ namespace json {
 namespace agi {
 class Options;
 
-DEFINE_BASE_EXCEPTION_NOINNER(MRUError,Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(MRUErrorInvalidKey, MRUError, "mru/invalid")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(MRUErrorIndexOutOfRange, MRUError, "mru/invalid")
+DEFINE_EXCEPTION(MRUError, Exception);
+DEFINE_EXCEPTION(MRUErrorInvalidKey, MRUError);
+DEFINE_EXCEPTION(MRUErrorIndexOutOfRange, MRUError);
 
 /// @class MRUManager
 /// @brief Most Recently Used (MRU) list handling
diff --git a/libaegisub/include/libaegisub/option.h b/libaegisub/include/libaegisub/option.h
index e45ba1b8a35b244fa4d1fa05c346f67225a3a9cb..e2a9d56dd8688c3b3132e86b5a8c156081a1d009 100644
--- a/libaegisub/include/libaegisub/option.h
+++ b/libaegisub/include/libaegisub/option.h
@@ -33,9 +33,9 @@ namespace json {
 
 namespace agi {
 
-DEFINE_BASE_EXCEPTION_NOINNER(OptionError,Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionErrorNotFound, OptionError, "options/not_found")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionErrorDuplicateKey, OptionError, "options/dump/duplicate")
+DEFINE_EXCEPTION(OptionError, Exception);
+DEFINE_EXCEPTION(OptionErrorNotFound, OptionError);
+DEFINE_EXCEPTION(OptionErrorDuplicateKey, OptionError);
 
 class OptionValue;
 
diff --git a/libaegisub/include/libaegisub/option_value.h b/libaegisub/include/libaegisub/option_value.h
index dcdcb0c23548209e9257b0b4bd28d09419bdc8a3..f9fda0fd91f6b701979dd52ad1528eb24ef29f75 100644
--- a/libaegisub/include/libaegisub/option_value.h
+++ b/libaegisub/include/libaegisub/option_value.h
@@ -27,8 +27,8 @@
 #undef Bool
 
 namespace agi {
-DEFINE_BASE_EXCEPTION_NOINNER(OptionValueError, Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(OptionValueErrorInvalidType, OptionValueError, "options/invalid_type")
+DEFINE_EXCEPTION(OptionValueError, Exception);
+DEFINE_EXCEPTION(OptionValueErrorInvalidType, OptionValueError);
 
 /// Option type
 /// No bitsets here.
@@ -64,7 +64,7 @@ class OptionValue {
 			case OptionType::ListColor:  return "List of Colors";
 			case OptionType::ListBool:   return "List of Bools";
 		}
-		throw agi::InternalError("Invalid option type", nullptr);
+		throw agi::InternalError("Invalid option type");
 	}
 
 	OptionValueErrorInvalidType TypeError(OptionType type) const {
diff --git a/libaegisub/include/libaegisub/vfr.h b/libaegisub/include/libaegisub/vfr.h
index b5d0da9686d460f069401cdd55bdefa2a905c372..1658539123806f2fe865e5aff3d029ac055b0ab9 100644
--- a/libaegisub/include/libaegisub/vfr.h
+++ b/libaegisub/include/libaegisub/vfr.h
@@ -39,17 +39,17 @@ enum Time {
 	END
 };
 
-DEFINE_BASE_EXCEPTION_NOINNER(Error, Exception)
+DEFINE_EXCEPTION(Error, Exception);
 /// FPS specified is not a valid frame rate
-DEFINE_SIMPLE_EXCEPTION_NOINNER(BadFPS, Error, "vfr/badfps")
+DEFINE_EXCEPTION(BadFPS, Error);
 /// Unknown timecode file format
-DEFINE_SIMPLE_EXCEPTION_NOINNER(UnknownFormat, Error, "vfr/timecodes/unknownformat")
+DEFINE_EXCEPTION(UnknownFormat, Error);
 /// Invalid line encountered in a timecode file
-DEFINE_SIMPLE_EXCEPTION_NOINNER(MalformedLine, Error, "vfr/timecodes/malformed")
+DEFINE_EXCEPTION(MalformedLine, Error);
 /// Timecode file or vector has too few timecodes to be usable
-DEFINE_SIMPLE_EXCEPTION_NOINNER(TooFewTimecodes, Error, "vfr/timecodes/toofew")
+DEFINE_EXCEPTION(TooFewTimecodes, Error);
 /// Timecode file or vector has timecodes that are not monotonically increasing
-DEFINE_SIMPLE_EXCEPTION_NOINNER(UnorderedTimecodes, Error, "vfr/timecodes/order")
+DEFINE_EXCEPTION(UnorderedTimecodes, Error);
 
 /// @class Framerate
 /// @brief Class for managing everything related to converting frames to times
diff --git a/libaegisub/lua/modules/lfs.cpp b/libaegisub/lua/modules/lfs.cpp
index c6a554dfe375f5a0497f2a04b85800ff5592f3e8..b3699291241b8d16c48ea19ee312d8ab76dc0b57 100644
--- a/libaegisub/lua/modules/lfs.cpp
+++ b/libaegisub/lua/modules/lfs.cpp
@@ -38,7 +38,7 @@ int wrap(lua_State *L) {
 	}
 	catch (agi::Exception const& e) {
 		lua_pushnil(L);
-		push_value(L, e.GetChainedMessage());
+		push_value(L, e.GetMessage());
 		return 2;
 	}
 	catch (error_tag) {
diff --git a/libaegisub/lua/script_reader.cpp b/libaegisub/lua/script_reader.cpp
index 5045b2608e01cf17ffd968ff96b8fc88d65a975d..df82b23d1bde051096cf9dd5141d94a63f726640 100644
--- a/libaegisub/lua/script_reader.cpp
+++ b/libaegisub/lua/script_reader.cpp
@@ -31,7 +31,7 @@ namespace agi { namespace lua {
 			filename = agi::fs::Canonicalize(raw_filename);
 		}
 		catch (agi::fs::FileSystemUnknownError const& e) {
-			LOG_E("auto4/lua") << "Error canonicalizing path: " << e.GetChainedMessage();
+			LOG_E("auto4/lua") << "Error canonicalizing path: " << e.GetMessage();
 		}
 
 		agi::read_file_mapping file(filename);
@@ -112,7 +112,7 @@ namespace agi { namespace lua {
 				// Not an error so swallow and continue on
 			}
 			catch (agi::Exception const& e) {
-				return error(L, "Error loading Lua module \"%s\":\n%s", path.string().c_str(), e.GetChainedMessage().c_str());
+				return error(L, "Error loading Lua module \"%s\":\n%s", path.string().c_str(), e.GetMessage().c_str());
 			}
 		}
 
diff --git a/src/ass_dialogue.cpp b/src/ass_dialogue.cpp
index 14e2d2d2545e9f0e952f44c808340f587145ce5e..80fe5618dfe04357630b3e6f2ffb320f9486a45c 100644
--- a/src/ass_dialogue.cpp
+++ b/src/ass_dialogue.cpp
@@ -81,7 +81,7 @@ public:
 
 	agi::StringRange next_tok() {
 		if (pos.eof())
-			throw SubtitleFormatParseError("Failed parsing line: " + std::string(str.begin(), str.end()), nullptr);
+			throw SubtitleFormatParseError("Failed parsing line: " + std::string(str.begin(), str.end()));
 		return *pos++;
 	}
 
@@ -100,7 +100,7 @@ void AssDialogue::Parse(std::string const& raw) {
 		str = agi::StringRange(raw.begin() + 9, raw.end());
 	}
 	else
-		throw SubtitleFormatParseError("Failed parsing line: " + raw, nullptr);
+		throw SubtitleFormatParseError("Failed parsing line: " + raw);
 
 	tokenizer tkn(str);
 
diff --git a/src/ass_override.cpp b/src/ass_override.cpp
index abcca5531fe1c7a16ad9d3dabcbe53accd83a27c..82954d11ad125e2a4b4f4f27f17fdbd68b1c204c 100644
--- a/src/ass_override.cpp
+++ b/src/ass_override.cpp
@@ -79,7 +79,7 @@ AssOverrideParameter::~AssOverrideParameter() {
 }
 
 template<> std::string AssOverrideParameter::Get<std::string>() const {
-	if (omitted) throw agi::InternalError("AssOverrideParameter::Get() called on omitted parameter", nullptr);
+	if (omitted) throw agi::InternalError("AssOverrideParameter::Get() called on omitted parameter");
 	if (block.get()) {
 		std::string str(block->GetText());
 		if (boost::starts_with(str, "{")) str.erase(begin(str));
diff --git a/src/ass_parser.cpp b/src/ass_parser.cpp
index 76c206c59aafbaf98905bf331b6ff3aff360b5a3..8ba47d0d401301b0f2f1697f45efa9a8b313e3f5 100644
--- a/src/ass_parser.cpp
+++ b/src/ass_parser.cpp
@@ -146,7 +146,7 @@ void AssParser::ParseScriptInfoLine(std::string const& data) {
 		else if (version_str == "v4.00+")
 			version = 1;
 		else
-			throw SubtitleFormatParseError("Unknown SSA file format version", nullptr);
+			throw SubtitleFormatParseError("Unknown SSA file format version");
 	}
 
 	// Nothing actually supports the Collisions property and malformed values
diff --git a/src/ass_style.cpp b/src/ass_style.cpp
index 901ddbeab704fc8632afdd849b049f123fa44b0d..e82514653449b98f062c6fb3afabd7745f270a66 100644
--- a/src/ass_style.cpp
+++ b/src/ass_style.cpp
@@ -56,7 +56,7 @@ class parser {
 
 	std::string next_tok() {
 		if (pos.eof())
-			throw SubtitleFormatParseError("Malformed style: not enough fields", nullptr);
+			throw SubtitleFormatParseError("Malformed style: not enough fields");
 		return agi::str(trim_copy(*pos++));
 	}
 
@@ -69,7 +69,7 @@ public:
 
 	void check_done() const {
 		if (!pos.eof())
-			throw SubtitleFormatParseError("Malformed style: too many fields", nullptr);
+			throw SubtitleFormatParseError("Malformed style: too many fields");
 	}
 
 	std::string next_str() { return next_tok(); }
@@ -80,7 +80,7 @@ public:
 			return boost::lexical_cast<int>(next_tok());
 		}
 		catch (boost::bad_lexical_cast const&) {
-			throw SubtitleFormatParseError("Malformed style: bad int field", nullptr);
+			throw SubtitleFormatParseError("Malformed style: bad int field");
 		}
 	}
 
@@ -89,7 +89,7 @@ public:
 			return boost::lexical_cast<double>(next_tok());
 		}
 		catch (boost::bad_lexical_cast const&) {
-			throw SubtitleFormatParseError("Malformed style: bad double field", nullptr);
+			throw SubtitleFormatParseError("Malformed style: bad double field");
 		}
 	}
 
diff --git a/src/async_video_provider.cpp b/src/async_video_provider.cpp
index 621335cd36ecd37eef0df0c89850b8102d016000..8a0a2dfd15b9827595f02b5ff7feec4fc3280abc 100644
--- a/src/async_video_provider.cpp
+++ b/src/async_video_provider.cpp
@@ -170,12 +170,12 @@ wxDEFINE_EVENT(EVT_VIDEO_ERROR, VideoProviderErrorEvent);
 wxDEFINE_EVENT(EVT_SUBTITLES_ERROR, SubtitlesProviderErrorEvent);
 
 VideoProviderErrorEvent::VideoProviderErrorEvent(VideoProviderError const& err)
-: agi::Exception(err.GetMessage(), &err)
+: agi::Exception(err.GetMessage())
 {
 	SetEventType(EVT_VIDEO_ERROR);
 }
 SubtitlesProviderErrorEvent::SubtitlesProviderErrorEvent(std::string const& err)
-: agi::Exception(err, nullptr)
+: agi::Exception(err)
 {
 	SetEventType(EVT_SUBTITLES_ERROR);
 }
diff --git a/src/async_video_provider.h b/src/async_video_provider.h
index 5da7e7ca18388e455e711df761544631ee2aa567..37232b978e12e1dbfc31b09f68e06e3530732555 100644
--- a/src/async_video_provider.h
+++ b/src/async_video_provider.h
@@ -134,16 +134,12 @@ struct FrameReadyEvent final : public wxEvent {
 // These exceptions are wxEvents so that they can be passed directly back to
 // the parent thread as events
 struct VideoProviderErrorEvent final : public wxEvent, public agi::Exception {
-	const char * GetName() const override { return "video/error"; }
 	wxEvent *Clone() const override { return new VideoProviderErrorEvent(*this); };
-	agi::Exception *Copy() const override { return new VideoProviderErrorEvent(*this); };
 	VideoProviderErrorEvent(VideoProviderError const& err);
 };
 
 struct SubtitlesProviderErrorEvent final : public wxEvent, public agi::Exception {
-	const char * GetName() const override { return "subtitles/error"; }
 	wxEvent *Clone() const override { return new SubtitlesProviderErrorEvent(*this); };
-	agi::Exception *Copy() const override { return new SubtitlesProviderErrorEvent(*this); };
 	SubtitlesProviderErrorEvent(std::string const& msg);
 };
 
diff --git a/src/audio_colorscheme.cpp b/src/audio_colorscheme.cpp
index bbcd7763e4b5f044f42eb0c18ada0a3c7a841399..80f35a15d77db2a7dc016f6c6d07a9d5c62cb305 100644
--- a/src/audio_colorscheme.cpp
+++ b/src/audio_colorscheme.cpp
@@ -46,7 +46,7 @@ AudioColorScheme::AudioColorScheme(int prec, std::string const& scheme_name, int
 		case AudioStyle_Inactive: opt_base += "Inactive/"; break;
 		case AudioStyle_Selected: opt_base += "Selection/"; break;
 		case AudioStyle_Primary:  opt_base += "Primary/"; break;
-		default: throw agi::InternalError("Unknown audio rendering styling", nullptr);
+		default: throw agi::InternalError("Unknown audio rendering styling");
 	}
 
 	double h_base  = OPT_GET(opt_base + "Hue Offset")->GetDouble();
diff --git a/src/audio_controller.h b/src/audio_controller.h
index b7d560c3975a9167fbb4a1ee59e84c91d9a853b9..11112c161118bc6f7630d0284f979d3aaf4ddad6 100644
--- a/src/audio_controller.h
+++ b/src/audio_controller.h
@@ -188,26 +188,26 @@ public:
 
 namespace agi {
 	/// Base class for all audio-related errors
-	DEFINE_BASE_EXCEPTION(AudioError, Exception)
+	DEFINE_EXCEPTION(AudioError, Exception);
 
 	/// Opening the audio failed for any reason
-	DEFINE_SIMPLE_EXCEPTION(AudioOpenError, AudioError, "audio/open")
+	DEFINE_EXCEPTION(AudioOpenError, AudioError);
 
 	/// There are no audio providers available to open audio files
-	DEFINE_SIMPLE_EXCEPTION(NoAudioProvidersError, AudioOpenError, "audio/open/no_providers")
+	DEFINE_EXCEPTION(NoAudioProvidersError, AudioOpenError);
 
 	/// The file exists, but no providers could find any audio tracks in it
-	DEFINE_SIMPLE_EXCEPTION(AudioDataNotFoundError, AudioOpenError, "audio/open/no_tracks")
+	DEFINE_EXCEPTION(AudioDataNotFoundError, AudioOpenError);
 
 	/// There are audio tracks, but no provider could actually read them
-	DEFINE_SIMPLE_EXCEPTION(AudioProviderOpenError, AudioOpenError, "audio/open/provider")
+	DEFINE_EXCEPTION(AudioProviderOpenError, AudioOpenError);
 
 	/// The audio cache failed to initialize
-	DEFINE_SIMPLE_EXCEPTION(AudioCacheOpenError, AudioOpenError, "audio/open/cache")
+	DEFINE_EXCEPTION(AudioCacheOpenError, AudioOpenError);
 
 	/// There are no audio players available
-	DEFINE_SIMPLE_EXCEPTION(NoAudioPlayersError, AudioOpenError, "audio/open/no_players")
+	DEFINE_EXCEPTION(NoAudioPlayersError, AudioOpenError);
 
 	/// The audio player failed to initialize
-	DEFINE_SIMPLE_EXCEPTION(AudioPlayerOpenError, AudioOpenError, "audio/open/player")
+	DEFINE_EXCEPTION(AudioPlayerOpenError, AudioOpenError);
 }
diff --git a/src/audio_player.cpp b/src/audio_player.cpp
index 4502afe6026a8c28de3785be98a44235c508f750..c4500bdff3bf4fe81034b795aa4ae3d4a089681e 100644
--- a/src/audio_player.cpp
+++ b/src/audio_player.cpp
@@ -89,7 +89,7 @@ std::vector<std::string> AudioPlayerFactory::GetClasses() {
 
 std::unique_ptr<AudioPlayer> AudioPlayerFactory::GetAudioPlayer(AudioProvider *provider, wxWindow *window) {
 	if (std::begin(factories) == std::end(factories))
-		throw agi::NoAudioPlayersError("No audio players are available.", nullptr);
+		throw agi::NoAudioPlayersError("No audio players are available.");
 
 	auto preferred = OPT_GET("Audio/Player")->GetString();
 	auto sorted = GetSorted(boost::make_iterator_range(std::begin(factories), std::end(factories)), preferred);
@@ -100,8 +100,8 @@ std::unique_ptr<AudioPlayer> AudioPlayerFactory::GetAudioPlayer(AudioProvider *p
 			return factory->create(provider, window);
 		}
 		catch (agi::AudioPlayerOpenError const& err) {
-			error += std::string(factory->name) + " factory: " + err.GetChainedMessage() + "\n";
+			error += std::string(factory->name) + " factory: " + err.GetMessage() + "\n";
 		}
 	}
-	throw agi::AudioPlayerOpenError(error, nullptr);
+	throw agi::AudioPlayerOpenError(error);
 }
diff --git a/src/audio_player_alsa.cpp b/src/audio_player_alsa.cpp
index fa97a4bb43b1652c88ece500d582448eb64bb396..fcd17124236591275da1565b6844741d43262ba4 100644
--- a/src/audio_player_alsa.cpp
+++ b/src/audio_player_alsa.cpp
@@ -295,7 +295,7 @@ AlsaPlayer::AlsaPlayer(AudioProvider *provider) try
 {
 }
 catch (std::system_error const&) {
-	throw agi::AudioPlayerOpenError("AlsaPlayer: Creating the playback thread failed", 0);
+	throw agi::AudioPlayerOpenError("AlsaPlayer: Creating the playback thread failed");
 }
 
 AlsaPlayer::~AlsaPlayer()
diff --git a/src/audio_player_dsound.cpp b/src/audio_player_dsound.cpp
index da6f41ba9f1fe14ffc0127139ac000e3b249e405..06ed2ca5d7dcb8a9e18c1893f00271f2ad74fc5f 100644
--- a/src/audio_player_dsound.cpp
+++ b/src/audio_player_dsound.cpp
@@ -102,7 +102,7 @@ DirectSoundPlayer::DirectSoundPlayer(AudioProvider *provider, wxWindow *parent)
 	// Initialize the DirectSound object
 	HRESULT res;
 	res = DirectSoundCreate8(&DSDEVID_DefaultPlayback,&directSound,nullptr); // TODO: support selecting audio device
-	if (FAILED(res)) throw agi::AudioPlayerOpenError("Failed initializing DirectSound", 0);
+	if (FAILED(res)) throw agi::AudioPlayerOpenError("Failed initializing DirectSound");
 
 	// Set DirectSound parameters
 	directSound->SetCooperativeLevel((HWND)parent->GetHandle(),DSSCL_PRIORITY);
@@ -133,11 +133,11 @@ DirectSoundPlayer::DirectSoundPlayer(AudioProvider *provider, wxWindow *parent)
 	// Create the buffer
 	IDirectSoundBuffer *buf;
 	res = directSound->CreateSoundBuffer(&desc,&buf,nullptr);
-	if (res != DS_OK) throw agi::AudioPlayerOpenError("Failed creating DirectSound buffer", 0);
+	if (res != DS_OK) throw agi::AudioPlayerOpenError("Failed creating DirectSound buffer");
 
 	// Copy interface to buffer
 	res = buf->QueryInterface(IID_IDirectSoundBuffer8,(LPVOID*) &buffer);
-	if (res != S_OK) throw agi::AudioPlayerOpenError("Failed casting interface to IDirectSoundBuffer8", 0);
+	if (res != S_OK) throw agi::AudioPlayerOpenError("Failed casting interface to IDirectSoundBuffer8");
 
 	// Set data
 	offset = 0;
diff --git a/src/audio_player_dsound2.cpp b/src/audio_player_dsound2.cpp
index 8d1ed3b25e8be08204adad8df254b72821255b7c..1e8081dba2f3d52965650e70aa99814cad1cc475 100644
--- a/src/audio_player_dsound2.cpp
+++ b/src/audio_player_dsound2.cpp
@@ -677,7 +677,7 @@ DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider, int
 	thread_handle = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, 0, 0);
 
 	if (!thread_handle)
-		throw agi::AudioPlayerOpenError("Failed creating playback thread in DirectSoundPlayer2. This is bad.", 0);
+		throw agi::AudioPlayerOpenError("Failed creating playback thread in DirectSoundPlayer2. This is bad.");
 
 	HANDLE running_or_error[] = { thread_running, error_happened };
 	switch (WaitForMultipleObjects(2, running_or_error, FALSE, INFINITE))
@@ -688,10 +688,10 @@ DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider, int
 
 	case WAIT_OBJECT_0 + 1:
 		// error happened, we fail
-		throw agi::AudioPlayerOpenError(error_message, 0);
+		throw agi::AudioPlayerOpenError(error_message);
 
 	default:
-		throw agi::AudioPlayerOpenError("Failed wait for thread start or thread error in DirectSoundPlayer2. This is bad.", 0);
+		throw agi::AudioPlayerOpenError("Failed wait for thread start or thread error in DirectSoundPlayer2. This is bad.");
 	}
 }
 
@@ -722,7 +722,7 @@ void DirectSoundPlayer2Thread::Play(int64_t start, int64_t count)
 	case WAIT_OBJECT_0+1: // Error
 		throw error_message;
 	default:
-		throw agi::InternalError("Unexpected result from WaitForMultipleObjects in DirectSoundPlayer2Thread::Play", 0);
+		throw agi::InternalError("Unexpected result from WaitForMultipleObjects in DirectSoundPlayer2Thread::Play");
 	}
 }
 
@@ -820,7 +820,7 @@ DirectSoundPlayer2::DirectSoundPlayer2(AudioProvider *provider, wxWindow *parent
 	catch (const char *msg)
 	{
 		LOG_E("audio/player/dsound") << msg;
-		throw agi::AudioPlayerOpenError(msg, 0);
+		throw agi::AudioPlayerOpenError(msg);
 	}
 }
 
diff --git a/src/audio_player_openal.cpp b/src/audio_player_openal.cpp
index fb298939dccc8f7edac927297999d430df216c0b..0b3b1ee7fd59d5f98e29bb6472728357bebf528c 100644
--- a/src/audio_player_openal.cpp
+++ b/src/audio_player_openal.cpp
@@ -61,7 +61,7 @@
 #pragma comment(lib, "openal32.lib")
 #endif
 
-DEFINE_SIMPLE_EXCEPTION(OpenALException, agi::AudioPlayerOpenError, "audio/open/player/openal")
+DEFINE_EXCEPTION(OpenALException, agi::AudioPlayerOpenError);
 
 namespace {
 class OpenALPlayer final : public AudioPlayer, wxTimer {
@@ -130,25 +130,25 @@ OpenALPlayer::OpenALPlayer(AudioProvider *provider)
 	try {
 		// Open device
 		device = alcOpenDevice(nullptr);
-		if (!device) throw OpenALException("Failed opening default OpenAL device", nullptr);
+		if (!device) throw OpenALException("Failed opening default OpenAL device");
 
 		// Create context
 		context = alcCreateContext(device, nullptr);
-		if (!context) throw OpenALException("Failed creating OpenAL context", nullptr);
-		if (!alcMakeContextCurrent(context)) throw OpenALException("Failed selecting OpenAL context", nullptr);
+		if (!context) throw OpenALException("Failed creating OpenAL context");
+		if (!alcMakeContextCurrent(context)) throw OpenALException("Failed selecting OpenAL context");
 
 		// Clear error code
 		alGetError();
 
 		// Generate buffers
 		alGenBuffers(num_buffers, buffers);
-		if (alGetError() != AL_NO_ERROR) throw OpenALException("Error generating OpenAL buffers", nullptr);
+		if (alGetError() != AL_NO_ERROR) throw OpenALException("Error generating OpenAL buffers");
 
 		// Generate source
 		alGenSources(1, &source);
 		if (alGetError() != AL_NO_ERROR) {
 			alDeleteBuffers(num_buffers, buffers);
-			throw OpenALException("Error generating OpenAL source", nullptr);
+			throw OpenALException("Error generating OpenAL source");
 		}
 	}
 	catch (...)
diff --git a/src/audio_player_oss.cpp b/src/audio_player_oss.cpp
index 1f915ef412a0160fc0405f88210e368f013dbd6c..3b7548c2487216dc62df59cb32d0c293f8c1bc90 100644
--- a/src/audio_player_oss.cpp
+++ b/src/audio_player_oss.cpp
@@ -54,7 +54,7 @@
 #endif
 
 namespace {
-DEFINE_SIMPLE_EXCEPTION(OSSError, agi::AudioPlayerOpenError, "audio/player/open/oss")
+DEFINE_EXCEPTION(OSSError, agi::AudioPlayerOpenError);
 class OSSPlayerThread;
 
 class OSSPlayer final : public AudioPlayer {
@@ -153,7 +153,7 @@ void OSSPlayer::OpenStream()
     wxString device = to_wx(OPT_GET("Player/Audio/OSS/Device")->GetString());
     dspdev = ::open(device.utf8_str(), O_WRONLY, 0);
     if (dspdev < 0) {
-        throw OSSError("OSS player: opening device failed", 0);
+        throw OSSError("OSS player: opening device failed");
     }
 
     // Use a reasonable buffer policy for low latency (OSS4)
@@ -165,7 +165,7 @@ void OSSPlayer::OpenStream()
     // Set number of channels
     int channels = provider->GetChannels();
     if (ioctl(dspdev, SNDCTL_DSP_CHANNELS, &channels) < 0) {
-        throw OSSError("OSS player: setting channels failed", 0);
+        throw OSSError("OSS player: setting channels failed");
     }
 
     // Set sample format
@@ -178,17 +178,17 @@ void OSSPlayer::OpenStream()
             sample_format = AFMT_S16_LE;
             break;
         default:
-            throw OSSError("OSS player: can only handle 8 and 16 bit sound", 0);
+            throw OSSError("OSS player: can only handle 8 and 16 bit sound");
     }
 
     if (ioctl(dspdev, SNDCTL_DSP_SETFMT, &sample_format) < 0) {
-        throw OSSError("OSS player: setting sample format failed", 0);
+        throw OSSError("OSS player: setting sample format failed");
     }
 
     // Set sample rate
     rate = provider->GetSampleRate();
     if (ioctl(dspdev, SNDCTL_DSP_SPEED, &rate) < 0) {
-        throw OSSError("OSS player: setting samplerate failed", 0);
+        throw OSSError("OSS player: setting samplerate failed");
     }
 }
 
diff --git a/src/audio_player_portaudio.cpp b/src/audio_player_portaudio.cpp
index 271c2933168cf9d3eed67dd02e538aab96e5fd15..b37b6e23b929b87996ec9bf6fcab445047159b34 100644
--- a/src/audio_player_portaudio.cpp
+++ b/src/audio_player_portaudio.cpp
@@ -44,7 +44,7 @@
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
-DEFINE_SIMPLE_EXCEPTION(PortAudioError, agi::AudioPlayerOpenError, "audio/player/open/portaudio")
+DEFINE_EXCEPTION(PortAudioError, agi::AudioPlayerOpenError);
 
 // Uncomment to enable extremely spammy debug logging
 //#define PORTAUDIO_DEBUG
diff --git a/src/audio_player_pulse.cpp b/src/audio_player_pulse.cpp
index 2593924fadb41310aaac53e28992d0c52e325dab..1c2ce2d9ceec77af3a6e444f0529d93eaf0b0cbb 100644
--- a/src/audio_player_pulse.cpp
+++ b/src/audio_player_pulse.cpp
@@ -101,7 +101,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 	// Initialise a mainloop
 	mainloop = pa_threaded_mainloop_new();
 	if (!mainloop)
-		throw agi::AudioPlayerOpenError("Failed to initialise PulseAudio threaded mainloop object", 0);
+		throw agi::AudioPlayerOpenError("Failed to initialise PulseAudio threaded mainloop object");
 
 	pa_threaded_mainloop_start(mainloop);
 
@@ -109,7 +109,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 	context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "Aegisub");
 	if (!context) {
 		pa_threaded_mainloop_free(mainloop);
-		throw agi::AudioPlayerOpenError("Failed to create PulseAudio context", 0);
+		throw agi::AudioPlayerOpenError("Failed to create PulseAudio context");
 	}
 	pa_context_set_state_callback(context, (pa_context_notify_cb_t)pa_context_notify, this);
 
@@ -127,7 +127,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 			pa_context_unref(context);
 			pa_threaded_mainloop_stop(mainloop);
 			pa_threaded_mainloop_free(mainloop);
-			throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror), 0);
+			throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
 		}
 		// otherwise loop once more
 	}
@@ -148,7 +148,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 		pa_context_unref(context);
 		pa_threaded_mainloop_stop(mainloop);
 		pa_threaded_mainloop_free(mainloop);
-		throw agi::AudioPlayerOpenError("PulseAudio could not create stream", 0);
+		throw agi::AudioPlayerOpenError("PulseAudio could not create stream");
 	}
 	pa_stream_set_state_callback(stream, (pa_stream_notify_cb_t)pa_stream_notify, this);
 	pa_stream_set_write_callback(stream, (pa_stream_request_cb_t)pa_stream_write, this);
@@ -157,7 +157,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 	paerror = pa_stream_connect_playback(stream, nullptr, nullptr, (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_NOT_MONOTONOUS|PA_STREAM_AUTO_TIMING_UPDATE), nullptr, nullptr);
 	if (paerror) {
 		LOG_E("audio/player/pulse") << "Stream connection failed: " << pa_strerror(paerror) << "(" << paerror << ")";
-		throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror), 0);
+		throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
 	}
 	while (true) {
 		stream_notify.Wait();
@@ -166,7 +166,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 		} else if (sstate == PA_STREAM_FAILED) {
 			paerror = pa_context_errno(context);
 			LOG_E("audio/player/pulse") << "Stream connection failed: " << pa_strerror(paerror) << "(" << paerror << ")";
-			throw agi::AudioPlayerOpenError("PulseAudio player: Something went wrong connecting the stream", 0);
+			throw agi::AudioPlayerOpenError("PulseAudio player: Something went wrong connecting the stream");
 		}
 	}
 }
diff --git a/src/audio_provider.cpp b/src/audio_provider.cpp
index ffafd41a907fa824a330039a69098e9002cfa337..58387e5394c8b2afc316610aba4ebae82e139d0a 100644
--- a/src/audio_provider.cpp
+++ b/src/audio_provider.cpp
@@ -44,7 +44,7 @@ void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count,
 
 	if (volume == 1.0) return;
 	if (bytes_per_sample != 2)
-		throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream", nullptr);
+		throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream");
 
 	short *buffer = static_cast<int16_t *>(buf);
 	for (size_t i = 0; i < (size_t)count; ++i)
@@ -80,7 +80,7 @@ void AudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const {
 		FillBuffer(buf, start, count);
 	}
 	catch (AudioDecodeError const& e) {
-		LOG_E("audio_provider") << e.GetChainedMessage();
+		LOG_E("audio_provider") << e.GetMessage();
 		ZeroFill(buf, count);
 		return;
 	}
@@ -144,19 +144,19 @@ std::unique_ptr<AudioProvider> AudioProviderFactory::GetProvider(agi::fs::path c
 			break;
 		}
 		catch (agi::fs::FileNotFound const& err) {
-			LOG_D("audio_provider") << err.GetChainedMessage();
+			LOG_D("audio_provider") << err.GetMessage();
 			msg_all += std::string(factory->name) + ": " + err.GetMessage() + " not found.\n";
 		}
 		catch (agi::AudioDataNotFoundError const& err) {
-			LOG_D("audio_provider") << err.GetChainedMessage();
+			LOG_D("audio_provider") << err.GetMessage();
 			found_file = true;
-			msg_all += std::string(factory->name) + ": " + err.GetChainedMessage() + "\n";
+			msg_all += std::string(factory->name) + ": " + err.GetMessage() + "\n";
 		}
 		catch (agi::AudioOpenError const& err) {
-			LOG_D("audio_provider") << err.GetChainedMessage();
+			LOG_D("audio_provider") << err.GetMessage();
 			found_audio = true;
 			found_file = true;
-			std::string thismsg = std::string(factory->name) + ": " + err.GetChainedMessage() + "\n";
+			std::string thismsg = std::string(factory->name) + ": " + err.GetMessage() + "\n";
 			msg_all += thismsg;
 			msg_partial += thismsg;
 		}
@@ -164,9 +164,9 @@ std::unique_ptr<AudioProvider> AudioProviderFactory::GetProvider(agi::fs::path c
 
 	if (!provider) {
 		if (found_audio)
-			throw agi::AudioProviderOpenError(msg_partial, nullptr);
+			throw agi::AudioProviderOpenError(msg_partial);
 		if (found_file)
-			throw agi::AudioDataNotFoundError(msg_all, nullptr);
+			throw agi::AudioDataNotFoundError(msg_all);
 		throw agi::fs::FileNotFound(filename);
 	}
 
@@ -187,5 +187,5 @@ std::unique_ptr<AudioProvider> AudioProviderFactory::GetProvider(agi::fs::path c
 	// Convert to HD
 	if (cache == 2) return CreateHDAudioProvider(std::move(provider));
 
-	throw agi::AudioCacheOpenError("Unknown caching method", nullptr);
+	throw agi::AudioCacheOpenError("Unknown caching method");
 }
diff --git a/src/audio_provider_avs.cpp b/src/audio_provider_avs.cpp
index 11c8922aa6859674d794fbc3e0116e0279fe3a66..24be20e106a6da13e78c3ca3e75b939061773241 100644
--- a/src/audio_provider_avs.cpp
+++ b/src/audio_provider_avs.cpp
@@ -89,22 +89,22 @@ AvisynthAudioProvider::AvisynthAudioProvider(agi::fs::path const& filename) {
 				LoadFromClip(env->Invoke("DirectShowSource", AVSValue(args, 3), argnames));
 			// Otherwise fail
 			else
-				throw agi::AudioProviderOpenError("No suitable audio source filter found. Try placing DirectShowSource.dll in the Aegisub application directory.", 0);
+				throw agi::AudioProviderOpenError("No suitable audio source filter found. Try placing DirectShowSource.dll in the Aegisub application directory.");
 		}
 	}
 	catch (AvisynthError &err) {
 		std::string errmsg(err.msg);
 		if (errmsg.find("filter graph manager won't talk to me") != errmsg.npos)
-			throw agi::AudioDataNotFoundError("Avisynth error: " + errmsg, 0);
+			throw agi::AudioDataNotFoundError("Avisynth error: " + errmsg);
 		else
-			throw agi::AudioProviderOpenError("Avisynth error: " + errmsg, 0);
+			throw agi::AudioProviderOpenError("Avisynth error: " + errmsg);
 	}
 }
 
 void AvisynthAudioProvider::LoadFromClip(AVSValue clip) {
 	// Check if it has audio
 	VideoInfo vi = clip.AsClip()->GetVideoInfo();
-	if (!vi.HasAudio()) throw agi::AudioDataNotFoundError("No audio found.", 0);
+	if (!vi.HasAudio()) throw agi::AudioDataNotFoundError("No audio found.");
 
 	IScriptEnvironment *env = avs_wrapper.GetEnv();
 
diff --git a/src/audio_provider_convert.cpp b/src/audio_provider_convert.cpp
index aae6c4f17378f50d36ec97ac721c78c6d63a4eb8..ac4c1af58bc8fbed40b013a7079c21733621a434 100644
--- a/src/audio_provider_convert.cpp
+++ b/src/audio_provider_convert.cpp
@@ -35,7 +35,7 @@ class BitdepthConvertAudioProvider final : public AudioProviderWrapper {
 public:
 	BitdepthConvertAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (bytes_per_sample > 8)
-			throw agi::AudioProviderOpenError("Audio format converter: audio with bitdepths greater than 64 bits/sample is currently unsupported", nullptr);
+			throw agi::AudioProviderOpenError("Audio format converter: audio with bitdepths greater than 64 bits/sample is currently unsupported");
 
 		src_bytes_per_sample = bytes_per_sample;
 		bytes_per_sample = sizeof(Target);
@@ -107,9 +107,9 @@ class DownmixAudioProvider final : public AudioProviderWrapper {
 public:
 	DownmixAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (bytes_per_sample != 2)
-			throw agi::InternalError("DownmixAudioProvider requires 16-bit input", nullptr);
+			throw agi::InternalError("DownmixAudioProvider requires 16-bit input");
 		if (channels == 1)
-			throw agi::InternalError("DownmixAudioProvider requires multi-channel input", nullptr);
+			throw agi::InternalError("DownmixAudioProvider requires multi-channel input");
 		src_channels = channels;
 		channels = 1;
 	}
@@ -137,9 +137,9 @@ class SampleDoublingAudioProvider final : public AudioProviderWrapper {
 public:
 	SampleDoublingAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (source->GetBytesPerSample() != 2)
-			throw agi::InternalError("UpsampleAudioProvider requires 16-bit input", nullptr);
+			throw agi::InternalError("UpsampleAudioProvider requires 16-bit input");
 		if (source->GetChannels() != 1)
-			throw agi::InternalError("UpsampleAudioProvider requires mono input", nullptr);
+			throw agi::InternalError("UpsampleAudioProvider requires mono input");
 
 		sample_rate *= 2;
 		num_samples *= 2;
diff --git a/src/audio_provider_ffmpegsource.cpp b/src/audio_provider_ffmpegsource.cpp
index 9672ab56a025d7955a7871044e640862d1f4e78b..9e9e9183fe0b535dd87cedb1695e701a3e1b9ea1 100644
--- a/src/audio_provider_ffmpegsource.cpp
+++ b/src/audio_provider_ffmpegsource.cpp
@@ -79,10 +79,10 @@ FFmpegSourceAudioProvider::FFmpegSourceAudioProvider(agi::fs::path const& filena
 	LoadAudio(filename);
 }
 catch (std::string const& err) {
-	throw agi::AudioProviderOpenError(err, nullptr);
+	throw agi::AudioProviderOpenError(err);
 }
 catch (const char *err) {
-	throw agi::AudioProviderOpenError(err, nullptr);
+	throw agi::AudioProviderOpenError(err);
 }
 
 void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
@@ -91,12 +91,12 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		if (ErrInfo.SubType == FFMS_ERROR_FILE_READ)
 			throw agi::fs::FileNotFound(std::string(ErrInfo.Buffer));
 		else
-			throw agi::AudioDataNotFoundError(ErrInfo.Buffer, nullptr);
+			throw agi::AudioDataNotFoundError(ErrInfo.Buffer);
 	}
 
 	std::map<int, std::string> TrackList = GetTracksOfType(Indexer, FFMS_TYPE_AUDIO);
 	if (TrackList.empty())
-		throw agi::AudioDataNotFoundError("no audio tracks found", nullptr);
+		throw agi::AudioDataNotFoundError("no audio tracks found");
 
 	// initialize the track number to an invalid value so we can detect later on
 	// whether the user actually had to choose a track or not
@@ -124,7 +124,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		if (TrackNumber < 0)
 			TrackNumber = FFMS_GetFirstTrackOfType(Index, FFMS_TYPE_AUDIO, &ErrInfo);
 		if (TrackNumber < 0)
-			throw agi::AudioDataNotFoundError(std::string("Couldn't find any audio tracks: ") + ErrInfo.Buffer, nullptr);
+			throw agi::AudioDataNotFoundError(std::string("Couldn't find any audio tracks: ") + ErrInfo.Buffer);
 
 		// index is valid and track number is now set,
 		// but do we have indexing info for the desired audio track?
@@ -166,7 +166,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 
 	AudioSource = FFMS_CreateAudioSource(filename.string().c_str(), TrackNumber, Index, -1, &ErrInfo);
 	if (!AudioSource)
-		throw agi::AudioProviderOpenError(std::string("Failed to open audio track: ") + ErrInfo.Buffer, nullptr);
+		throw agi::AudioProviderOpenError(std::string("Failed to open audio track: ") + ErrInfo.Buffer);
 
 	const FFMS_AudioProperties AudioInfo = *FFMS_GetAudioProperties(AudioSource);
 
@@ -175,7 +175,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 	num_samples = AudioInfo.NumSamples;
 	decoded_samples = AudioInfo.NumSamples;
 	if (channels <= 0 || sample_rate <= 0 || num_samples <= 0)
-		throw agi::AudioProviderOpenError("sanity check failed, consult your local psychiatrist", nullptr);
+		throw agi::AudioProviderOpenError("sanity check failed, consult your local psychiatrist");
 
 	switch (AudioInfo.SampleFormat) {
 		case FFMS_FMT_U8:  bytes_per_sample = 1; float_samples = false; break;
@@ -184,7 +184,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		case FFMS_FMT_FLT: bytes_per_sample = 4; float_samples = true; break;
 		case FFMS_FMT_DBL: bytes_per_sample = 8; float_samples = true; break;
 		default:
-			throw agi::AudioProviderOpenError("unknown or unsupported sample format", nullptr);
+			throw agi::AudioProviderOpenError("unknown or unsupported sample format");
 	}
 
 #if FFMS_VERSION >= ((2 << 24) | (17 << 16) | (4 << 8) | 0)
diff --git a/src/audio_provider_hd.cpp b/src/audio_provider_hd.cpp
index bc191af82b8e6d14d705fe0c575411c9a4264647..d22c81be9a916bd9dffb28afd956a6d9978079df 100644
--- a/src/audio_provider_hd.cpp
+++ b/src/audio_provider_hd.cpp
@@ -62,7 +62,7 @@ public:
 
 		// Check free space
 		if ((uint64_t)num_samples * bytes_per_sample > agi::fs::FreeSpace(cache_dir))
-			throw agi::AudioCacheOpenError("Not enough free disk space in " + cache_dir.string() + " to cache the audio", nullptr);
+			throw agi::AudioCacheOpenError("Not enough free disk space in " + cache_dir.string() + " to cache the audio");
 
 		auto filename = agi::format("audio-%lld-%lld", time(nullptr),
 			boost::interprocess::ipcdetail::get_current_process_id());
diff --git a/src/audio_provider_pcm.cpp b/src/audio_provider_pcm.cpp
index 1530774a3a84fad2d8a08aa3f4764c6e3cc9038a..945dc007b0a85b222ad163a8b062701ed11fc9d1 100644
--- a/src/audio_provider_pcm.cpp
+++ b/src/audio_provider_pcm.cpp
@@ -155,9 +155,9 @@ public:
 
 		// Check magic values
 		if (!CheckFourcc(header.ch.type, "RIFF"))
-			throw agi::AudioDataNotFoundError("File is not a RIFF file", nullptr);
+			throw agi::AudioDataNotFoundError("File is not a RIFF file");
 		if (!CheckFourcc(header.format, "WAVE"))
-			throw agi::AudioDataNotFoundError("File is not a RIFF WAV file", nullptr);
+			throw agi::AudioDataNotFoundError("File is not a RIFF WAV file");
 
 		// How far into the file we have processed.
 		// Must be incremented by the riff chunk size fields.
@@ -177,13 +177,13 @@ public:
 			filepos += sizeof(ch);
 
 			if (CheckFourcc(ch.type, "fmt ")) {
-				if (got_fmt_header) throw agi::AudioProviderOpenError("Invalid file, multiple 'fmt ' chunks", nullptr);
+				if (got_fmt_header) throw agi::AudioProviderOpenError("Invalid file, multiple 'fmt ' chunks");
 				got_fmt_header = true;
 
 				auto const& fmt = Read<fmtChunk>(filepos);
 
 				if (fmt.compression != 1)
-					throw agi::AudioProviderOpenError("Can't use file, not PCM encoding", nullptr);
+					throw agi::AudioProviderOpenError("Can't use file, not PCM encoding");
 
 				// Set stuff inherited from the AudioProvider class
 				sample_rate = fmt.samplerate;
@@ -194,7 +194,7 @@ public:
 				// This won't pick up 'data' chunks inside 'wavl' chunks
 				// since the 'wavl' chunks wrap those.
 
-				if (!got_fmt_header) throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.", nullptr);
+				if (!got_fmt_header) throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.");
 
 				auto samples = std::min(total_data - filepos, ch.size) / bytes_per_sample / channels;
 				index_points.push_back(IndexPoint{filepos, samples});
@@ -280,16 +280,16 @@ public:
 		size_t smallest_possible_file = sizeof(RiffChunk) + sizeof(FormatChunk) + sizeof(DataChunk);
 
 		if (file->size() < smallest_possible_file)
-			throw agi::AudioDataNotFoundError("File is too small to be a Wave64 file", nullptr);
+			throw agi::AudioDataNotFoundError("File is too small to be a Wave64 file");
 
 		// Read header
 		auto const& header = Read<RiffChunk>(0);
 
 		// Check magic values
 		if (!CheckGuid(header.riff_guid, w64GuidRIFF))
-			throw agi::AudioDataNotFoundError("File is not a Wave64 RIFF file", nullptr);
+			throw agi::AudioDataNotFoundError("File is not a Wave64 RIFF file");
 		if (!CheckGuid(header.format_guid, w64GuidWAVE))
-			throw agi::AudioDataNotFoundError("File is not a Wave64 WAVE file", nullptr);
+			throw agi::AudioDataNotFoundError("File is not a Wave64 WAVE file");
 
 		// How far into the file we have processed.
 		// Must be incremented by the riff chunk size fields.
@@ -310,11 +310,11 @@ public:
 
 			if (CheckGuid(chunk_guid, w64Guidfmt)) {
 				if (got_fmt_header)
-					throw agi::AudioProviderOpenError("Bad file, found more than one 'fmt' chunk", nullptr);
+					throw agi::AudioProviderOpenError("Bad file, found more than one 'fmt' chunk");
 
 				auto const& fmt = Read<FormatChunk>(filepos);
 				if (fmt.format.wFormatTag != 1)
-					throw agi::AudioProviderOpenError("File is not uncompressed PCM", nullptr);
+					throw agi::AudioProviderOpenError("File is not uncompressed PCM");
 
 				got_fmt_header = true;
 				// Set stuff inherited from the AudioProvider class
@@ -324,7 +324,7 @@ public:
 			}
 			else if (CheckGuid(chunk_guid, w64Guiddata)) {
 				if (!got_fmt_header)
-					throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.", nullptr);
+					throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.");
 
 				auto samples = chunk_size / bytes_per_sample / channels;
 				index_points.push_back(IndexPoint{
@@ -369,7 +369,7 @@ std::unique_ptr<AudioProvider> CreatePCMAudioProvider(agi::fs::path const& filen
 	}
 
 	if (wrong_file_type)
-		throw agi::AudioDataNotFoundError(msg, nullptr);
+		throw agi::AudioDataNotFoundError(msg);
 	else
-		throw agi::AudioProviderOpenError(msg, nullptr);
+		throw agi::AudioProviderOpenError(msg);
 }
diff --git a/src/audio_provider_ram.cpp b/src/audio_provider_ram.cpp
index b03b4648a7773d11cfbf7ba9e405fb32989b3f80..1a3a4e174560730d5d2a31325a98d3fa5056e1a1 100644
--- a/src/audio_provider_ram.cpp
+++ b/src/audio_provider_ram.cpp
@@ -68,7 +68,7 @@ public:
 			blockcache.resize((source->GetNumSamples() * source->GetBytesPerSample() + CacheBlockSize - 1) >> CacheBits);
 		}
 		catch (std::bad_alloc const&) {
-			throw agi::AudioCacheOpenError("Couldn't open audio, not enough ram available.", nullptr);
+			throw agi::AudioCacheOpenError("Couldn't open audio, not enough ram available.");
 		}
 
 		decoder = std::thread([&] {
diff --git a/src/auto4_base.cpp b/src/auto4_base.cpp
index f2af62ba7248bc4f1bc4c028b33c8e67298635fc..8bb753d62e3ac000124a79903d3efb4f2c7bb1e0 100644
--- a/src/auto4_base.cpp
+++ b/src/auto4_base.cpp
@@ -446,7 +446,7 @@ namespace Automation4 {
 	void ScriptFactory::Register(std::unique_ptr<ScriptFactory> factory)
 	{
 		if (find(Factories().begin(), Factories().end(), factory) != Factories().end())
-			throw agi::InternalError("Automation 4: Attempt to register the same script factory multiple times. This should never happen.", nullptr);
+			throw agi::InternalError("Automation 4: Attempt to register the same script factory multiple times. This should never happen.");
 
 		Factories().emplace_back(std::move(factory));
 	}
diff --git a/src/auto4_base.h b/src/auto4_base.h
index be67d3f7c9a8fa662435345e16bf245d0b00698e..5f645ccd0036befcbe54d4ad61f6a88f5d439ba7 100644
--- a/src/auto4_base.h
+++ b/src/auto4_base.h
@@ -54,9 +54,9 @@ namespace agi { struct Context; }
 namespace cmd { class Command; }
 
 namespace Automation4 {
-	DEFINE_BASE_EXCEPTION_NOINNER(AutomationError, agi::Exception)
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(ScriptLoadError, AutomationError, "automation/load/generic")
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(MacroRunError, AutomationError, "automation/macro/generic")
+	DEFINE_EXCEPTION(AutomationError, agi::Exception);
+	DEFINE_EXCEPTION(ScriptLoadError, AutomationError);
+	DEFINE_EXCEPTION(MacroRunError, AutomationError);
 
 	// Calculate the extents of a text string given a style
 	bool CalculateTextExtents(AssStyle *style, std::string const& text, double &width, double &height, double &descent, double &extlead);
diff --git a/src/auto4_lua_assfile.cpp b/src/auto4_lua_assfile.cpp
index 63ad6c475cddac8137ee1c8d7a6cf1b76dab0ef2..1eea59131179a03f19fcc4601f09e3965f998081 100644
--- a/src/auto4_lua_assfile.cpp
+++ b/src/auto4_lua_assfile.cpp
@@ -54,7 +54,7 @@
 namespace {
 	using namespace agi::lua;
 
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(BadField, Automation4::MacroRunError, "automation/macro/bad_field")
+	DEFINE_EXCEPTION(BadField, Automation4::MacroRunError);
 	BadField bad_field(const char *expected_type, const char *name, const char *line_clasee)
 	{
 		return BadField(std::string("Invalid or missing field '") + name + "' in '" + line_clasee + "' class subtitle line (expected " + expected_type + ")");
diff --git a/src/command/app.cpp b/src/command/app.cpp
index d6ca8f2162c8a3230395a330b29bdc46a57bba7c..418a0f8b75ab0552e4183a0da50c555e3f4582f4 100644
--- a/src/command/app.cpp
+++ b/src/command/app.cpp
@@ -211,7 +211,7 @@ struct app_options final : public Command {
 		try {
 			while (Preferences(c->parent).ShowModal() < 0);
 		} catch (agi::Exception& e) {
-			LOG_E("config/init") << "Caught exception: " << e.GetName() << " -> " << e.GetMessage();
+			LOG_E("config/init") << "Caught exception: " << e.GetMessage();
 		}
 	}
 };
diff --git a/src/command/command.h b/src/command/command.h
index e3ea33f542afd4e47406893ed96e3544712c6a3d..f8ad391e13be3cba7abf981b178d2d7bb4905f96 100644
--- a/src/command/command.h
+++ b/src/command/command.h
@@ -53,8 +53,8 @@ struct cname final : public Command {                         \
 
 /// Commands
 namespace cmd {
-DEFINE_BASE_EXCEPTION_NOINNER(CommandError, agi::Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(CommandNotFound, CommandError, "command/notfound")
+DEFINE_EXCEPTION(CommandError, agi::Exception);
+DEFINE_EXCEPTION(CommandNotFound, CommandError);
 
 	enum CommandFlags {
 		/// Default command type
diff --git a/src/command/tool.cpp b/src/command/tool.cpp
index 82ce746a3d9ca8859a9ee0d57aa0cdbcda62c144..a94d24cb2fd165ac6411c5d1aaae049a05857735 100644
--- a/src/command/tool.cpp
+++ b/src/command/tool.cpp
@@ -207,7 +207,7 @@ struct tool_translation_assistant final : public Command {
 			c->dialog->ShowModal<DialogTranslation>(c);
 		}
 		catch (agi::Exception const& e) {
-			wxMessageBox(to_wx(e.GetChainedMessage()));
+			wxMessageBox(to_wx(e.GetMessage()));
 		}
 	}
 };
diff --git a/src/dialog_fonts_collector.cpp b/src/dialog_fonts_collector.cpp
index c82e763e82d736a6d0c2023ce6be025c2cea50e1..d37d8945a7903ec15f040c16fb05834129eec8b9 100644
--- a/src/dialog_fonts_collector.cpp
+++ b/src/dialog_fonts_collector.cpp
@@ -123,7 +123,7 @@ void FontsCollectorThread(AssFile *subs, agi::fs::path const& destination, FcMod
 			}
 			catch (agi::fs::FileSystemError const& e) {
 				AppendText(wxString::Format(_("* Failed to create directory '%s': %s.\n"),
-					destination.parent_path().wstring(), to_wx(e.GetChainedMessage())), 2);
+					destination.parent_path().wstring(), to_wx(e.GetMessage())), 2);
 				collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
 				return;
 			}
diff --git a/src/dialog_progress.cpp b/src/dialog_progress.cpp
index 50172c86be5817e686eb5b10f2d0f3945566489d..d09efb1db54554c41cf9d29e97804c963adc0dc5 100644
--- a/src/dialog_progress.cpp
+++ b/src/dialog_progress.cpp
@@ -154,7 +154,7 @@ void DialogProgress::Run(std::function<void(agi::ProgressSink*)> task, int prior
 			task(this->ps);
 		}
 		catch (agi::Exception const& e) {
-			this->ps->Log(e.GetChainedMessage());
+			this->ps->Log(e.GetMessage());
 		}
 
 		Main().Async([this]{
diff --git a/src/dialog_shift_times.cpp b/src/dialog_shift_times.cpp
index f9d3cb0b34af54fbf9b66434d6256811519933d9..717428b7e761c67f50f0c160a0ddd2d8bb2a0819 100644
--- a/src/dialog_shift_times.cpp
+++ b/src/dialog_shift_times.cpp
@@ -323,7 +323,7 @@ void DialogShiftTimes::SaveHistory(json::Array const& shifted_blocks) {
 		json::Writer::Write(history, agi::io::Save(history_filename).Get());
 	}
 	catch (agi::fs::FileSystemError const& e) {
-		LOG_E("dialog_shift_times/save_history") << "Cannot save shift times history: " << e.GetChainedMessage();
+		LOG_E("dialog_shift_times/save_history") << "Cannot save shift times history: " << e.GetMessage();
 	}
 }
 
@@ -341,7 +341,7 @@ void DialogShiftTimes::LoadHistory() {
 			history_box->Append(get_history_string(history_entry));
 	}
 	catch (agi::fs::FileSystemError const& e) {
-		LOG_D("dialog_shift_times/load_history") << "Cannot load shift times history: " << e.GetChainedMessage();
+		LOG_D("dialog_shift_times/load_history") << "Cannot load shift times history: " << e.GetMessage();
 	}
 	catch (...) {
 		history_box->Thaw();
diff --git a/src/dialog_style_manager.cpp b/src/dialog_style_manager.cpp
index eeaa728051b73a592a8f159f730ec23f8621eec6..07bf4f268558a463a87b0b5c5798191f88382089 100644
--- a/src/dialog_style_manager.cpp
+++ b/src/dialog_style_manager.cpp
@@ -684,7 +684,7 @@ void DialogStyleManager::OnCurrentImport() {
 			reader->ReadFile(&temp, filename, 0, charset);
 	}
 	catch (agi::Exception const& err) {
-		wxMessageBox(to_wx(err.GetChainedMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, this);
+		wxMessageBox(to_wx(err.GetMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, this);
 	}
 	catch (...) {
 		wxMessageBox("Unknown error", "Error", wxOK | wxICON_ERROR | wxCENTER, this);
diff --git a/src/dialog_translation.h b/src/dialog_translation.h
index 72918b131d2da2308c3d940430bd80d1cc35ba80..c18ba9939abdf675f4ae6d876ddcd98b1c29e1ea 100644
--- a/src/dialog_translation.h
+++ b/src/dialog_translation.h
@@ -79,5 +79,5 @@ public:
 	void Commit(bool next);
 	void InsertOriginal();
 
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(NothingToTranslate, agi::Exception, "dialog/translation/nothing_to_translate");
+	DEFINE_EXCEPTION(NothingToTranslate, agi::Exception);;
 };
diff --git a/src/dialog_version_check.cpp b/src/dialog_version_check.cpp
index 8aef014f08cbba9096d7efcac685f4cd55637e5a..865d32b2d131905834e23027218d4e7d7c86fae0 100644
--- a/src/dialog_version_check.cpp
+++ b/src/dialog_version_check.cpp
@@ -163,7 +163,7 @@ void VersionCheckerResultDialog::OnClose(wxCloseEvent &) {
 	Destroy();
 }
 
-DEFINE_SIMPLE_EXCEPTION_NOINNER(VersionCheckError, agi::Exception, "versioncheck")
+DEFINE_EXCEPTION(VersionCheckError, agi::Exception);
 
 void PostErrorEvent(bool interactive, wxString const& error_text) {
 	if (interactive) {
diff --git a/src/help_button.cpp b/src/help_button.cpp
index 5a3c006584e3b4cc548cf3fab61da9126d0d8ea9..674b4215e44f54e6bfb73a0806ffbb5759c0189f 100644
--- a/src/help_button.cpp
+++ b/src/help_button.cpp
@@ -69,7 +69,7 @@ HelpButton::HelpButton(wxWindow *parent, wxString const& page, wxPoint position,
 	Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OpenPage(page); });
 	init_static();
 	if (pages.find(page) == pages.end())
-		throw agi::InternalError("Invalid help page", nullptr);
+		throw agi::InternalError("Invalid help page");
 }
 
 void HelpButton::OpenPage(wxString const& pageID) {
diff --git a/src/hotkey.cpp b/src/hotkey.cpp
index 36570d3a52eaa0443c3b406fde7f6d80832fcfa7..aa333739fa42401b17e55ecb03437987237cbdc6 100644
--- a/src/hotkey.cpp
+++ b/src/hotkey.cpp
@@ -172,7 +172,7 @@ bool check(std::string const& context, agi::Context *c, wxKeyEvent &evt) {
 		return true;
 	}
 	catch (cmd::CommandNotFound const& e) {
-		wxMessageBox(to_wx(e.GetChainedMessage()), _("Invalid command name for hotkey"),
+		wxMessageBox(to_wx(e.GetMessage()), _("Invalid command name for hotkey"),
 			wxOK | wxICON_ERROR | wxCENTER | wxSTAY_ON_TOP);
 		return true;
 	}
diff --git a/src/hotkey_data_view_model.cpp b/src/hotkey_data_view_model.cpp
index c94011bd338362df111cbd1780617ffd618c679a..59308d5ec441a1eec5d1332b7eddb9a419380ef5 100644
--- a/src/hotkey_data_view_model.cpp
+++ b/src/hotkey_data_view_model.cpp
@@ -105,11 +105,11 @@ public:
 				variant = cmd::get(combo.CmdName())->StrHelp();
 			}
 			catch (agi::Exception const& e) {
-				variant = to_wx(e.GetChainedMessage());
+				variant = to_wx(e.GetMessage());
 			}
 		}
 		else
-			throw agi::InternalError("HotkeyDataViewModel asked for an invalid column number", nullptr);
+			throw agi::InternalError("HotkeyDataViewModel asked for an invalid column number");
 	}
 
 	bool SetValue(wxVariant const& variant, unsigned int col) override {
diff --git a/src/include/aegisub/audio_provider.h b/src/include/aegisub/audio_provider.h
index f16c5cf54cb8bbf0f89cb37db32cce08c540f586..5ea04c6eaff6e709e94939b51ffa75dee17c8fb3 100644
--- a/src/include/aegisub/audio_provider.h
+++ b/src/include/aegisub/audio_provider.h
@@ -94,6 +94,6 @@ struct AudioProviderFactory {
 	static std::unique_ptr<AudioProvider> GetProvider(agi::fs::path const& filename, agi::BackgroundRunner *br);
 };
 
-DEFINE_BASE_EXCEPTION_NOINNER(AudioProviderError, agi::Exception)
+DEFINE_EXCEPTION(AudioProviderError, agi::Exception);
 /// Error of some sort occurred while decoding a frame
-DEFINE_SIMPLE_EXCEPTION_NOINNER(AudioDecodeError, AudioProviderError, "audio/error")
+DEFINE_EXCEPTION(AudioDecodeError, AudioProviderError);
diff --git a/src/include/aegisub/menu.h b/src/include/aegisub/menu.h
index b3b784efbbe09d1b78368685b2e6ba961ec50633..266a3c669a83af65da917c61f88ffa7df2361f24 100644
--- a/src/include/aegisub/menu.h
+++ b/src/include/aegisub/menu.h
@@ -29,9 +29,9 @@ class wxMenuBar;
 class wxWindow;
 
 namespace menu {
-	DEFINE_BASE_EXCEPTION_NOINNER(Error, agi::Exception)
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(UnknownMenu, Error, "menu/unknown")
-	DEFINE_SIMPLE_EXCEPTION_NOINNER(InvalidMenu, Error, "menu/invalid")
+	DEFINE_EXCEPTION(Error, agi::Exception);
+	DEFINE_EXCEPTION(UnknownMenu, Error);
+	DEFINE_EXCEPTION(InvalidMenu, Error);
 
 	/// @brief Get the menu with the specified name as a wxMenuBar
 	/// @param name Name of the menu
diff --git a/src/include/aegisub/video_provider.h b/src/include/aegisub/video_provider.h
index e0f81a916b3b12d6fa2e6883aa2931f06ea42bda..d257a6b1c02794bf35635488cdf07a3f570ad866 100644
--- a/src/include/aegisub/video_provider.h
+++ b/src/include/aegisub/video_provider.h
@@ -86,11 +86,11 @@ public:
 	virtual bool HasAudio() const { return false; }
 };
 
-DEFINE_BASE_EXCEPTION_NOINNER(VideoProviderError, agi::Exception)
+DEFINE_EXCEPTION(VideoProviderError, agi::Exception);
 /// File could be opened, but is not a supported format
-DEFINE_SIMPLE_EXCEPTION_NOINNER(VideoNotSupported, VideoProviderError, "video/open/notsupported")
+DEFINE_EXCEPTION(VideoNotSupported, VideoProviderError);
 /// File appears to be a supported format, but could not be opened
-DEFINE_SIMPLE_EXCEPTION_NOINNER(VideoOpenError, VideoProviderError, "video/open/failed")
+DEFINE_EXCEPTION(VideoOpenError, VideoProviderError);
 
 /// Error of some sort occurred while decoding a frame
-DEFINE_SIMPLE_EXCEPTION_NOINNER(VideoDecodeError, VideoProviderError, "video/error")
+DEFINE_EXCEPTION(VideoDecodeError, VideoProviderError);
diff --git a/src/main.cpp b/src/main.cpp
index 357f2eb3076555016e323f8e4e4dc1885c0cb1e4..5714f9f40a9fbc3da7a6d75d8f723fb4c18c332f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -198,7 +198,7 @@ bool AegisubApp::OnInit() {
 		boost::interprocess::ibufferstream stream((const char *)default_config, sizeof(default_config));
 		config::opt->ConfigNext(stream);
 	} catch (agi::Exception& e) {
-		LOG_E("config/init") << "Caught exception: " << e.GetName() << " -> " << e.GetMessage();
+		LOG_E("config/init") << "Caught exception: " << e.GetMessage();
 	}
 
 	try {
@@ -421,7 +421,7 @@ bool AegisubApp::OnExceptionInMainLoop() {
 		throw;
 	}
 	catch (const agi::Exception &e) {
-		SHOW_EXCEPTION(to_wx(e.GetChainedMessage()));
+		SHOW_EXCEPTION(to_wx(e.GetMessage()));
 	}
 	catch (const std::exception &e) {
 		SHOW_EXCEPTION(to_wx(e.what()));
@@ -450,7 +450,7 @@ int AegisubApp::OnRun() {
 	catch (const wxString &err) { error = from_wx(err); }
 	catch (const char *err) { error = err; }
 	catch (const std::exception &e) { error = std::string("std::exception: ") + e.what(); }
-	catch (const agi::Exception &e) { error = "agi::exception: " + e.GetChainedMessage(); }
+	catch (const agi::Exception &e) { error = "agi::exception: " + e.GetMessage(); }
 	catch (...) { error = "Program terminated in error."; }
 
 	// Report errors
diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp
index 78aee3f6d66e9d199870a03870f02d62dda41498..0d1fa2b6f78dffdc558315f3c6819f0b70621055 100644
--- a/src/mkv_wrap.cpp
+++ b/src/mkv_wrap.cpp
@@ -71,7 +71,7 @@ struct MkvStdIO final : InputStream {
 			memcpy(buffer, self->file.read(pos, count), count);
 		}
 		catch (agi::Exception const& e) {
-			self->error = e.GetChainedMessage();
+			self->error = e.GetMessage();
 			return -1;
 		}
 
@@ -90,7 +90,7 @@ struct MkvStdIO final : InputStream {
 			}
 		}
 		catch (agi::Exception const& e) {
-			self->error = e.GetChainedMessage();
+			self->error = e.GetMessage();
 		}
 
 		return -1;
diff --git a/src/mkv_wrap.h b/src/mkv_wrap.h
index e7a3406f13430ccc2cde4c49bff7b21d5a29c404..88d21f6932f6d169d770051185552bae13287e98 100644
--- a/src/mkv_wrap.h
+++ b/src/mkv_wrap.h
@@ -22,7 +22,7 @@
 #include <libaegisub/exception.h>
 #include <libaegisub/fs_fwd.h>
 
-DEFINE_SIMPLE_EXCEPTION_NOINNER(MatroskaException, agi::Exception, "matroksa_wrapper/generic")
+DEFINE_EXCEPTION(MatroskaException, agi::Exception);
 
 class AssFile;
 
diff --git a/src/preferences.h b/src/preferences.h
index 46e3305da08ce970036240d12317f014394a53af..bedaa2b1b8c1db613b853015874e11e1a79bfb25 100644
--- a/src/preferences.h
+++ b/src/preferences.h
@@ -30,9 +30,9 @@ class wxButton;
 class wxTreebook;
 namespace agi { class OptionValue; }
 
-DEFINE_BASE_EXCEPTION_NOINNER(PreferencesError, agi::Exception)
-DEFINE_SIMPLE_EXCEPTION_NOINNER(PreferenceIncorrectType, PreferencesError, "preferences/incorrect_type")
-DEFINE_SIMPLE_EXCEPTION_NOINNER(PreferenceNotSupported, PreferencesError, "preferences/not_supported")
+DEFINE_EXCEPTION(PreferencesError, agi::Exception);
+DEFINE_EXCEPTION(PreferenceIncorrectType, PreferencesError);
+DEFINE_EXCEPTION(PreferenceNotSupported, PreferencesError);
 
 class Preferences final : public wxDialog {
 public:
diff --git a/src/project.cpp b/src/project.cpp
index ce0b1fb4e437833c6f0837c25faa8790750700a0..395fd52f7a116db032826b0ae9d2c9383d049f74 100644
--- a/src/project.cpp
+++ b/src/project.cpp
@@ -124,7 +124,7 @@ void Project::DoLoadSubtitles(agi::fs::path const& path, std::string encoding) {
 		return ShowError(path.string() + " not found.");
 	}
 	catch (agi::Exception const& e) {
-		return ShowError(e.GetChainedMessage());
+		return ShowError(e.GetMessage());
 	}
 	catch (std::exception const& e) {
 		return ShowError(std::string(e.what()));
@@ -226,21 +226,21 @@ void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
 		}
 	}
 	catch (agi::fs::FileNotFound const& e) {
-		return ShowError(_("The audio file was not found: ") + to_wx(e.GetChainedMessage()));
+		return ShowError(_("The audio file was not found: ") + to_wx(e.GetMessage()));
 	}
 	catch (agi::AudioDataNotFoundError const& e) {
 		if (quiet) {
-			LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetChainedMessage();
+			LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetMessage();
 			return;
 		}
 		else
-			return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetChainedMessage()));
+			return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
 	}
 	catch (agi::AudioProviderOpenError const& e) {
-		return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetChainedMessage()));
+		return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
 	}
 	catch (agi::Exception const& e) {
-		return ShowError(e.GetChainedMessage());
+		return ShowError(e.GetMessage());
 	}
 
 	SetPath(audio_file, "?audio", "Audio", path);
@@ -336,11 +336,11 @@ void Project::LoadTimecodes(agi::fs::path const& path) {
 		DoLoadTimecodes(path);
 	}
 	catch (agi::fs::FileSystemError const& e) {
-		ShowError(e.GetChainedMessage());
+		ShowError(e.GetMessage());
 		config::mru->Remove("Timecodes", path);
 	}
 	catch (agi::vfr::Error const& e) {
-		ShowError("Failed to parse timecodes file: " + e.GetChainedMessage());
+		ShowError("Failed to parse timecodes file: " + e.GetMessage());
 		config::mru->Remove("Timecodes", path);
 	}
 }
@@ -362,11 +362,11 @@ void Project::LoadKeyframes(agi::fs::path const& path) {
 		DoLoadKeyframes(path);
 	}
 	catch (agi::fs::FileSystemError const& e) {
-		ShowError(e.GetChainedMessage());
+		ShowError(e.GetMessage());
 		config::mru->Remove("Keyframes", path);
 	}
 	catch (agi::keyframe::Error const& e) {
-		ShowError("Failed to parse keyframes file: " + e.GetChainedMessage());
+		ShowError("Failed to parse keyframes file: " + e.GetMessage());
 		config::mru->Remove("Keyframes", path);
 	}
 }
diff --git a/src/resolution_resampler.cpp b/src/resolution_resampler.cpp
index 3f116f7480d1a33c39931bf480b0710c2aa9b8b1..2f79be4a2ea0d96dde12bfe200adf2a9d493820a 100644
--- a/src/resolution_resampler.cpp
+++ b/src/resolution_resampler.cpp
@@ -200,7 +200,7 @@ namespace {
 			case YCbCrMatrix::tv_fcc:  case YCbCrMatrix::pc_fcc:  return agi::ycbcr_matrix::fcc;
 			case YCbCrMatrix::tv_240m: case YCbCrMatrix::pc_240m: return agi::ycbcr_matrix::smpte_240m;
 		}
-		throw agi::InternalError("Invalid matrix", nullptr);
+		throw agi::InternalError("Invalid matrix");
 	}
 
 	agi::ycbcr_range range(YCbCrMatrix mat) {
@@ -217,7 +217,7 @@ namespace {
 			case YCbCrMatrix::pc_240m:
 				return agi::ycbcr_range::pc;
 		}
-		throw agi::InternalError("Invalid matrix", nullptr);
+		throw agi::InternalError("Invalid matrix");
 	}
 }
 
diff --git a/src/search_replace_engine.cpp b/src/search_replace_engine.cpp
index 6f5804b3e94cda423677b1c671fe50b6382136cf..9faf90db92aea00a1ee303a25aeba138102095c1 100644
--- a/src/search_replace_engine.cpp
+++ b/src/search_replace_engine.cpp
@@ -39,7 +39,7 @@ auto get_dialogue_field(SearchReplaceSettings::Field field) -> decltype(&AssDial
 		case SearchReplaceSettings::Field::ACTOR: return &AssDialogueBase::Actor;
 		case SearchReplaceSettings::Field::EFFECT: return &AssDialogueBase::Effect;
 	}
-	throw agi::InternalError("Bad field for search", nullptr);
+	throw agi::InternalError("Bad field for search");
 }
 
 std::string const& get_normalized(const AssDialogue *diag, decltype(&AssDialogueBase::Text) field) {
diff --git a/src/subtitle_format.cpp b/src/subtitle_format.cpp
index 2b5741a7667a12f94c26be3c98b9982deb6f9a05..f8531c93f85ec110618e38535388b997783d952e 100644
--- a/src/subtitle_format.cpp
+++ b/src/subtitle_format.cpp
@@ -150,7 +150,7 @@ agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte, a
 		case 10: return Framerate(120000, 1001); break;
 		case 11: return Framerate(120, 1);       break;
 	}
-	throw agi::InternalError("Out of bounds result from wxGetSingleChoiceIndex?", nullptr);
+	throw agi::InternalError("Out of bounds result from wxGetSingleChoiceIndex?");
 }
 
 void SubtitleFormat::StripTags(AssFile &file) {
@@ -277,7 +277,7 @@ template<class Cont, class Pred>
 SubtitleFormat *find_or_throw(Cont &container, Pred pred) {
 	auto it = find_if(container.begin(), container.end(), pred);
 	if (it == container.end())
-		throw UnknownSubtitleFormatError("Subtitle format for extension not found", nullptr);
+		throw UnknownSubtitleFormatError("Subtitle format for extension not found");
 	return it->get();
 }
 
diff --git a/src/subtitle_format.h b/src/subtitle_format.h
index 41b97e55c5cf3140bb92fa6700876c47612e62bb..bb2878942f82ac1ec0addf1e858be60f79134a70 100644
--- a/src/subtitle_format.h
+++ b/src/subtitle_format.h
@@ -118,5 +118,5 @@ public:
 	static void LoadFormats();
 };
 
-DEFINE_SIMPLE_EXCEPTION(SubtitleFormatParseError, agi::InvalidInputException, "subtitle_io/parse/generic")
-DEFINE_SIMPLE_EXCEPTION(UnknownSubtitleFormatError, agi::InvalidInputException, "subtitle_io/unknown")
+DEFINE_EXCEPTION(SubtitleFormatParseError, agi::InvalidInputException);
+DEFINE_EXCEPTION(UnknownSubtitleFormatError, agi::InvalidInputException);
diff --git a/src/subtitle_format_ass.cpp b/src/subtitle_format_ass.cpp
index 9fa15fafd842289be1299ea4faaf57fd7944ffd6..107884a7bbf52c6d60b0f04b3172497eff3c7bde 100644
--- a/src/subtitle_format_ass.cpp
+++ b/src/subtitle_format_ass.cpp
@@ -31,7 +31,7 @@
 #include <libaegisub/ass/uuencode.h>
 #include <libaegisub/fs.h>
 
-DEFINE_SIMPLE_EXCEPTION(AssParseError, SubtitleFormatParseError, "subtitle_io/parse/ass")
+DEFINE_EXCEPTION(AssParseError, SubtitleFormatParseError);
 
 void AssSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename, agi::vfr::Framerate const& fps, std::string const& encoding) const {
 	TextFileReader file(filename, encoding);
@@ -44,7 +44,7 @@ void AssSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename,
 			parser.AddLine(line);
 		}
 		catch (const char *err) {
-			throw AssParseError("Error processing line: " + line + ": " + err, nullptr);
+			throw AssParseError("Error processing line: " + line + ": " + err);
 		}
 	}
 }
diff --git a/src/subtitle_format_ebu3264.cpp b/src/subtitle_format_ebu3264.cpp
index 34258098a0c060c010b34e5006fc328a5b7b84bc..9b97e9bbd75c75801585aab49fd3df6bef4088d2 100644
--- a/src/subtitle_format_ebu3264.cpp
+++ b/src/subtitle_format_ebu3264.cpp
@@ -404,7 +404,7 @@ namespace
 			else if (!imline.CheckLineLengths(export_settings.max_line_length))
 			{
 				if (export_settings.line_wrapping_mode == EbuExportSettings::AbortOverLength)
-					throw Ebu3264SubtitleFormat::ConversionFailed(from_wx(wxString::Format(_("Line over maximum length: %s"), line.Text.get())), nullptr);
+					throw Ebu3264SubtitleFormat::ConversionFailed(from_wx(wxString::Format(_("Line over maximum length: %s"), line.Text.get())));
 				else // skip over-long lines
 					subs_list.pop_back();
 			}
diff --git a/src/subtitle_format_ebu3264.h b/src/subtitle_format_ebu3264.h
index c6b3b39b7aeb10b45e91667f463b4bcd432f87e9..afa07fa510313ba1760976338873286e433e6c78 100644
--- a/src/subtitle_format_ebu3264.h
+++ b/src/subtitle_format_ebu3264.h
@@ -30,5 +30,5 @@ public:
 	std::vector<std::string> GetWriteWildcards() const override;
 	void WriteFile(const AssFile *src, agi::fs::path const& filename, agi::vfr::Framerate const& fps, std::string const& encoding) const override;
 
-	DEFINE_SIMPLE_EXCEPTION(ConversionFailed, agi::InvalidInputException, "subtitle_io/ebu3264/conversion_error")
+	DEFINE_EXCEPTION(ConversionFailed, agi::InvalidInputException);
 };
diff --git a/src/subtitle_format_srt.cpp b/src/subtitle_format_srt.cpp
index 2b8817d35e283026a609e31e6c929d77241e1a1b..aa1d5c9108e69cd38136411bd56b2daf6107bb08 100644
--- a/src/subtitle_format_srt.cpp
+++ b/src/subtitle_format_srt.cpp
@@ -51,7 +51,7 @@
 #include <boost/regex.hpp>
 #include <map>
 
-DEFINE_SIMPLE_EXCEPTION(SRTParseError, SubtitleFormatParseError, "subtitle_io/parse/srt")
+DEFINE_EXCEPTION(SRTParseError, SubtitleFormatParseError);
 
 namespace {
 class SrtTagParser {
@@ -382,11 +382,11 @@ void SRTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename,
 				if (regex_search(text_line, timestamp_match, timestamp_regex))
 					goto found_timestamps;
 
-				throw SRTParseError(agi::format("Parsing SRT: Expected subtitle index at line %d", line_num), nullptr);
+				throw SRTParseError(agi::format("Parsing SRT: Expected subtitle index at line %d", line_num));
 
 			case STATE_TIMESTAMP:
 				if (!regex_search(text_line, timestamp_match, timestamp_regex))
-					throw SRTParseError(agi::format("Parsing SRT: Expected timestamp pair at line %d", line_num), nullptr);
+					throw SRTParseError(agi::format("Parsing SRT: Expected timestamp pair at line %d", line_num));
 found_timestamps:
 				if (line) {
 					// finalize active line
@@ -452,7 +452,7 @@ found_timestamps:
 	}
 
 	if (state == 1 || state == 2)
-		throw SRTParseError("Parsing SRT: Incomplete file", nullptr);
+		throw SRTParseError("Parsing SRT: Incomplete file");
 
 	if (line) // an unfinalized line
 		line->Text = tag_parser.ToAss(text);
diff --git a/src/subtitle_format_ssa.h b/src/subtitle_format_ssa.h
index 66ba45d03e906530e9a89c9e424926781f4ea30a..5083834d3774746c48a17ef403f95f345e9b794b 100644
--- a/src/subtitle_format_ssa.h
+++ b/src/subtitle_format_ssa.h
@@ -23,4 +23,4 @@ public:
 	/// @todo Not actually true
 	bool CanSave(const AssFile*) const override { return true; }
 	void WriteFile(const AssFile *src, agi::fs::path const& filename, agi::vfr::Framerate const& fps, std::string const& encoding) const override;
-};
\ No newline at end of file
+};
diff --git a/src/subtitle_format_ttxt.cpp b/src/subtitle_format_ttxt.cpp
index 6d28560747eb43e6a3ff966db50599fff1e252b1..26391e072e673e32d0b82a4986c0f5e79e708ae4 100644
--- a/src/subtitle_format_ttxt.cpp
+++ b/src/subtitle_format_ttxt.cpp
@@ -43,7 +43,7 @@
 #include <boost/range/adaptor/reversed.hpp>
 #include <wx/xml/xml.h>
 
-DEFINE_SIMPLE_EXCEPTION(TTXTParseError, SubtitleFormatParseError, "subtitle_io/parse/ttxt")
+DEFINE_EXCEPTION(TTXTParseError, SubtitleFormatParseError);
 
 TTXTSubtitleFormat::TTXTSubtitleFormat()
 : SubtitleFormat("MPEG-4 Streaming Text")
@@ -63,10 +63,10 @@ void TTXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename
 
 	// Load XML document
 	wxXmlDocument doc;
-	if (!doc.Load(filename.wstring())) throw TTXTParseError("Failed loading TTXT XML file.", nullptr);
+	if (!doc.Load(filename.wstring())) throw TTXTParseError("Failed loading TTXT XML file.");
 
 	// Check root node name
-	if (doc.GetRoot()->GetName() != "TextStream") throw TTXTParseError("Invalid TTXT file.", nullptr);
+	if (doc.GetRoot()->GetName() != "TextStream") throw TTXTParseError("Invalid TTXT file.");
 
 	// Check version
 	wxString verStr = doc.GetRoot()->GetAttribute("version", "");
@@ -76,7 +76,7 @@ void TTXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename
 	else if (verStr == "1.1")
 		version = 1;
 	else
-		throw TTXTParseError("Unknown TTXT version: " + from_wx(verStr), nullptr);
+		throw TTXTParseError("Unknown TTXT version: " + from_wx(verStr));
 
 	// Get children
 	AssDialogue *diag = nullptr;
diff --git a/src/subtitle_format_txt.cpp b/src/subtitle_format_txt.cpp
index 2e0bd98c3f2c4c84c595838f66e0cafeefbb657c..707dd76f819f640f52280010efff5a737c459424 100644
--- a/src/subtitle_format_txt.cpp
+++ b/src/subtitle_format_txt.cpp
@@ -81,7 +81,7 @@ void TXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename,
 
 		// Check if this isn't a timecodes file
 		if (boost::starts_with(value, "# timecode"))
-			throw SubtitleFormatParseError("File is a timecode file, cannot load as subtitles.", nullptr);
+			throw SubtitleFormatParseError("File is a timecode file, cannot load as subtitles.");
 
 		// Read comment data
 		bool isComment = false;
diff --git a/src/subtitles_provider_csri.cpp b/src/subtitles_provider_csri.cpp
index 58eb0e33bd2dde5ac816bf2208a941ccc2eee07d..87429e213c34a202a22002e712ac8958d9009980 100644
--- a/src/subtitles_provider_csri.cpp
+++ b/src/subtitles_provider_csri.cpp
@@ -83,7 +83,7 @@ CSRISubtitlesProvider::CSRISubtitlesProvider(std::string type) {
 	}
 
 	if (!renderer)
-		throw agi::InternalError("CSRI renderer vanished between initial list and creation?", 0);
+		throw agi::InternalError("CSRI renderer vanished between initial list and creation?");
 }
 
 void CSRISubtitlesProvider::DrawSubtitles(VideoFrame &dst, double time) {
diff --git a/src/thesaurus.cpp b/src/thesaurus.cpp
index dbf5be3a4556f0ef9e9c1a036c736271c8b6c59e..7f0b396f5f88d6897d5d7ceb2e94304e92959e68 100644
--- a/src/thesaurus.cpp
+++ b/src/thesaurus.cpp
@@ -106,7 +106,7 @@ void Thesaurus::OnLanguageChanged() {
 			});
 		}
 		catch (agi::Exception const& e) {
-			LOG_E("thesaurus") << e.GetChainedMessage();
+			LOG_E("thesaurus") << e.GetMessage();
 		}
 	});
 }
diff --git a/src/toolbar.cpp b/src/toolbar.cpp
index d0d5c86df4b60916381920465ff77af695076584..9ad945056d70d232352371d2bd84e8095ae3d1af 100644
--- a/src/toolbar.cpp
+++ b/src/toolbar.cpp
@@ -106,7 +106,7 @@ namespace {
 			auto root_it = root.find(name);
 			if (root_it == root.end()) {
 				// Toolbar names are all hardcoded so this should never happen
-				throw agi::InternalError("Toolbar named " + name + " not found.", nullptr);
+				throw agi::InternalError("Toolbar named " + name + " not found.");
 			}
 
 			json::Array const& arr = root_it->second;
diff --git a/src/utils.cpp b/src/utils.cpp
index f6cf5497b589906ae7b151495719ab793d596f50..6eb8afc3f03f73afd3249ae694c135842b239abe 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -196,7 +196,7 @@ void CleanCache(agi::fs::path const& directory, std::string const& file_type, ui
 				LOG_D("utils/clean_cache") << "deleted " << i.second;
 			}
 			catch  (agi::Exception const& e) {
-				LOG_D("utils/clean_cache") << "failed to delete file " << i.second << ": " << e.GetChainedMessage();
+				LOG_D("utils/clean_cache") << "failed to delete file " << i.second << ": " << e.GetMessage();
 				continue;
 			}
 
diff --git a/src/validators.cpp b/src/validators.cpp
index c86c8bb03ddef60b5d080caea7382e6138306b14..f839722bedf8561c9035ef0984309c2cd237e83a 100644
--- a/src/validators.cpp
+++ b/src/validators.cpp
@@ -175,7 +175,7 @@ bool StringBinder::TransferFromWindow() {
 	else if (wxComboBox *ctrl = dynamic_cast<wxComboBox*>(window))
 		*value = from_wx(ctrl->GetValue());
 	else
-		throw agi::InternalError("Unsupported control type", nullptr);
+		throw agi::InternalError("Unsupported control type");
 	return true;
 }
 
@@ -186,6 +186,6 @@ bool StringBinder::TransferToWindow() {
 	else if (wxComboBox *ctrl = dynamic_cast<wxComboBox*>(window))
 		ctrl->SetValue(to_wx(*value));
 	else
-		throw agi::InternalError("Unsupported control type", nullptr);
+		throw agi::InternalError("Unsupported control type");
 	return true;
 }
diff --git a/src/validators.h b/src/validators.h
index 501de5acdd6678059fa2f77660ea6a6d617327aa..07afe60373dbcc53873fd2d8bd563af9b1e50807 100644
--- a/src/validators.h
+++ b/src/validators.h
@@ -84,7 +84,7 @@ class EnumBinder final : public wxValidator {
 		else if (auto rb = dynamic_cast<wxComboBox*>(GetWindow()))
 			*value = static_cast<T>(rb->GetSelection());
 		else
-			throw agi::InternalError("Control type not supported by EnumBinder", nullptr);
+			throw agi::InternalError("Control type not supported by EnumBinder");
 		return true;
 	}
 
@@ -94,7 +94,7 @@ class EnumBinder final : public wxValidator {
 		else if (auto cb = dynamic_cast<wxComboBox*>(GetWindow()))
 			cb->SetSelection(static_cast<int>(*value));
 		else
-			throw agi::InternalError("Control type not supported by EnumBinder", nullptr);
+			throw agi::InternalError("Control type not supported by EnumBinder");
 		return true;
 	}
 
diff --git a/src/video_controller.cpp b/src/video_controller.cpp
index dc25101f48e5bff7250f59c24c81f134b6f9a062..c5468e6f343b64cd9f81377538bd6ccce706af04 100644
--- a/src/video_controller.cpp
+++ b/src/video_controller.cpp
@@ -196,7 +196,7 @@ double VideoController::GetARFromType(AspectRatio type) const {
 		case AspectRatio::Widescreen: return 16.0/9.0;
 		case AspectRatio::Cinematic:  return 2.35;
 	}
-	throw agi::InternalError("Bad AR type", nullptr);
+	throw agi::InternalError("Bad AR type");
 }
 
 void VideoController::SetAspectRatio(double value) {
diff --git a/src/video_display.cpp b/src/video_display.cpp
index 6d5344de4c78de68ff49d8defe1af743c757d7fd..83268e3dc3a5db7e5e2916f9b271339f083f7e7f 100644
--- a/src/video_display.cpp
+++ b/src/video_display.cpp
@@ -67,16 +67,12 @@
 /// Attribute list for gl canvases; set the canvases to doublebuffered rgba with an 8 bit stencil buffer
 int attribList[] = { WX_GL_RGBA , WX_GL_DOUBLEBUFFER, WX_GL_STENCIL_SIZE, 8, 0 };
 
-/// @class VideoOutRenderException
-/// @extends VideoOutException
-/// @brief An OpenGL error occurred while uploading or displaying a frame
+/// An OpenGL error occurred while uploading or displaying a frame
 class OpenGlException final : public agi::Exception {
 public:
 	OpenGlException(const char *func, int err)
 	: agi::Exception(from_wx(wxString::Format("%s failed with error code %d", func, err)))
 	{ }
-	const char * GetName() const override { return "video/opengl"; }
-	Exception * Copy() const override { return new OpenGlException(*this); }
 };
 
 #define E(cmd) cmd; if (GLenum err = glGetError()) throw OpenGlException(#cmd, err)
@@ -224,7 +220,7 @@ catch (const agi::Exception &err) {
 	wxLogError(
 		"An error occurred trying to render the video frame on the screen.\n"
 		"Error message reported: %s",
-		err.GetChainedMessage());
+		err.GetMessage());
 	con->project->CloseVideo();
 }
 
diff --git a/src/video_out_gl.h b/src/video_out_gl.h
index 885160050a061ff19f10031ce4ac11236eb0eff5..7d12f5dfd9bc9805ab5fa9102a6a345f05125631 100644
--- a/src/video_out_gl.h
+++ b/src/video_out_gl.h
@@ -75,37 +75,26 @@ public:
 	/// @param height Height in pixels of viewport
 	void Render(int x, int y, int width, int height);
 
-	/// @brief Constructor
 	VideoOutGL();
-	/// @brief Destructor
 	~VideoOutGL();
 };
 
-/// @class VideoOutException
-/// @extends Aegisub::Exception
-/// @brief Base class for all exceptions thrown by VideoOutGL
-DEFINE_BASE_EXCEPTION_NOINNER(VideoOutException, agi::Exception)
+/// Base class for all exceptions thrown by VideoOutGL
+DEFINE_EXCEPTION(VideoOutException, agi::Exception);
 
-/// @class VideoOutRenderException
-/// @extends VideoOutException
-/// @brief An OpenGL error occurred while uploading or displaying a frame
+/// An OpenGL error occurred while uploading or displaying a frame
 class VideoOutRenderException final : public VideoOutException {
 public:
 	VideoOutRenderException(const char *func, int err)
 	: VideoOutException(std::string(func) + " failed with error code " + std::to_string(err))
 	{ }
-	const char * GetName() const override { return "videoout/opengl/render"; }
-	Exception * Copy() const override { return new VideoOutRenderException(*this); }
 };
-/// @class VideoOutOpenGLException
-/// @extends VideoOutException
-/// @brief An OpenGL error occurred while setting up the video display
+
+/// An OpenGL error occurred while setting up the video display
 class VideoOutInitException final : public VideoOutException {
 public:
 	VideoOutInitException(const char *func, int err)
 	: VideoOutException(std::string(func) + " failed with error code " + std::to_string(err))
 	{ }
 	VideoOutInitException(const char *err) : VideoOutException(err) { }
-	const char * GetName() const override { return "videoout/opengl/init"; }
-	Exception * Copy() const override { return new VideoOutInitException(*this); }
 };