diff --git a/src/json.hpp b/src/json.hpp
index 5ee3edad..522c601d 100644
--- a/src/json.hpp
+++ b/src/json.hpp
@@ -116,7 +116,7 @@ namespace detail
 
 Extension of std::exception objects with a member @a id for exception ids.
 
-@note To have nothrow-copy-constructible exceptions, we inherit from
+@note To have nothrow-copy-constructible exceptions, we internally use
       std::runtime_error which can cope with arbitrary-length error messages.
       Intermediate strings are built with static functions and then passed to
       the actual constructor.
@@ -136,16 +136,17 @@ class exception : public std::exception
     const int id;
 
   protected:
-    exception(int id_, const char* what_arg)
+    exception(int id_, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
         : id(id_), m(what_arg)
     {}
 
-    static std::string name(const std::string& ename, int id_)
+    static std::string name(const std::string& ename, int id)
     {
-        return "[json.exception." + ename + "." + std::to_string(id_) + "] ";
+        return "[json.exception." + ename + "." + std::to_string(id) + "] ";
     }
 
   private:
+    /// an exception object as storage for error messages
     std::runtime_error m;
 };
 
@@ -184,23 +185,23 @@ json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last
 
 @since version 3.0.0
 */
-class parse_error : public exception
+class parse_error : private exception
 {
   public:
     /*!
     @brief create a parse error exception
-    @param[in] id_        the id of the exception
+    @param[in] id         the id of the exception
     @param[in] byte_      the byte index where the error occured (or 0 if
                           the position cannot be determined)
     @param[in] what_arg   the explanatory string
     @return parse_error object
     */
-    static parse_error create(int id_, size_t byte_, const std::string& what_arg)
+    static parse_error create(int id, size_t byte_, const std::string& what_arg)
     {
-        std::string w = exception::name("parse_error", id_) + "parse error" +
+        std::string w = exception::name("parse_error", id) + "parse error" +
                         (byte_ != 0 ? (" at " + std::to_string(byte_)) : "") +
                         ": " + what_arg;
-        return parse_error(id_, byte_, w.c_str());
+        return parse_error(id, byte_, w.c_str());
     }
 
     /*!
@@ -216,9 +217,8 @@ class parse_error : public exception
     const size_t byte;
 
   private:
-    parse_error(int id_, size_t byte_, const char* what_arg)
-        : exception(id_, what_arg),
-          byte(byte_)
+    parse_error(int id, size_t byte_, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg), byte(byte_)
     {}
 };
 
@@ -249,15 +249,15 @@ json.exception.invalid_iterator.214 | cannot get value | Cannot get value for it
 class invalid_iterator : public exception
 {
   public:
-    static invalid_iterator create(int id_, const std::string& what_arg)
+    static invalid_iterator create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("invalid_iterator", id_) + what_arg;
-        return invalid_iterator(id_, w.c_str());
+        std::string w = exception::name("invalid_iterator", id) + what_arg;
+        return invalid_iterator(id, w.c_str());
     }
 
   private:
-    invalid_iterator(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    invalid_iterator(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -288,15 +288,15 @@ json.exception.type_error.315 | values in object must be primitive | The @ref un
 class type_error : public exception
 {
   public:
-    static type_error create(int id_, const std::string& what_arg)
+    static type_error create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("type_error", id_) + what_arg;
-        return type_error(id_, w.c_str());
+        std::string w = exception::name("type_error", id) + what_arg;
+        return type_error(id, w.c_str());
     }
 
   private:
-    type_error(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    type_error(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -319,15 +319,15 @@ json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed n
 class out_of_range : public exception
 {
   public:
-    static out_of_range create(int id_, const std::string& what_arg)
+    static out_of_range create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("out_of_range", id_) + what_arg;
-        return out_of_range(id_, w.c_str());
+        std::string w = exception::name("out_of_range", id) + what_arg;
+        return out_of_range(id, w.c_str());
     }
 
   private:
-    out_of_range(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    out_of_range(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -345,15 +345,15 @@ json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "valu
 class other_error : public exception
 {
   public:
-    static other_error create(int id_, const std::string& what_arg)
+    static other_error create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("other_error", id_) + what_arg;
-        return other_error(id_, w.c_str());
+        std::string w = exception::name("other_error", id) + what_arg;
+        return other_error(id, w.c_str());
     }
 
   private:
-    other_error(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    other_error(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c
index a51f77e7..c7243ca4 100644
--- a/src/json.hpp.re2c
+++ b/src/json.hpp.re2c
@@ -116,7 +116,7 @@ namespace detail
 
 Extension of std::exception objects with a member @a id for exception ids.
 
-@note To have nothrow-copy-constructible exceptions, we inherit from
+@note To have nothrow-copy-constructible exceptions, we internally use
       std::runtime_error which can cope with arbitrary-length error messages.
       Intermediate strings are built with static functions and then passed to
       the actual constructor.
@@ -136,16 +136,17 @@ class exception : public std::exception
     const int id;
 
   protected:
-    exception(int id_, const char* what_arg)
+    exception(int id_, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
         : id(id_), m(what_arg)
     {}
 
-    static std::string name(const std::string& ename, int id_)
+    static std::string name(const std::string& ename, int id)
     {
-        return "[json.exception." + ename + "." + std::to_string(id_) + "] ";
+        return "[json.exception." + ename + "." + std::to_string(id) + "] ";
     }
 
   private:
+    /// an exception object as storage for error messages
     std::runtime_error m;
 };
 
@@ -189,18 +190,18 @@ class parse_error : public exception
   public:
     /*!
     @brief create a parse error exception
-    @param[in] id_        the id of the exception
+    @param[in] id         the id of the exception
     @param[in] byte_      the byte index where the error occured (or 0 if
                           the position cannot be determined)
     @param[in] what_arg   the explanatory string
     @return parse_error object
     */
-    static parse_error create(int id_, size_t byte_, const std::string& what_arg)
+    static parse_error create(int id, size_t byte_, const std::string& what_arg)
     {
-        std::string w = exception::name("parse_error", id_) + "parse error" +
+        std::string w = exception::name("parse_error", id) + "parse error" +
                         (byte_ != 0 ? (" at " + std::to_string(byte_)) : "") +
                         ": " + what_arg;
-        return parse_error(id_, byte_, w.c_str());
+        return parse_error(id, byte_, w.c_str());
     }
 
     /*!
@@ -216,9 +217,8 @@ class parse_error : public exception
     const size_t byte;
 
   private:
-    parse_error(int id_, size_t byte_, const char* what_arg)
-        : exception(id_, what_arg),
-          byte(byte_)
+    parse_error(int id, size_t byte_, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg), byte(byte_)
     {}
 };
 
@@ -249,15 +249,15 @@ json.exception.invalid_iterator.214 | cannot get value | Cannot get value for it
 class invalid_iterator : public exception
 {
   public:
-    static invalid_iterator create(int id_, const std::string& what_arg)
+    static invalid_iterator create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("invalid_iterator", id_) + what_arg;
-        return invalid_iterator(id_, w.c_str());
+        std::string w = exception::name("invalid_iterator", id) + what_arg;
+        return invalid_iterator(id, w.c_str());
     }
 
   private:
-    invalid_iterator(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    invalid_iterator(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -288,15 +288,15 @@ json.exception.type_error.315 | values in object must be primitive | The @ref un
 class type_error : public exception
 {
   public:
-    static type_error create(int id_, const std::string& what_arg)
+    static type_error create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("type_error", id_) + what_arg;
-        return type_error(id_, w.c_str());
+        std::string w = exception::name("type_error", id) + what_arg;
+        return type_error(id, w.c_str());
     }
 
   private:
-    type_error(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    type_error(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -319,15 +319,15 @@ json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed n
 class out_of_range : public exception
 {
   public:
-    static out_of_range create(int id_, const std::string& what_arg)
+    static out_of_range create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("out_of_range", id_) + what_arg;
-        return out_of_range(id_, w.c_str());
+        std::string w = exception::name("out_of_range", id) + what_arg;
+        return out_of_range(id, w.c_str());
     }
 
   private:
-    out_of_range(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    out_of_range(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
@@ -345,15 +345,15 @@ json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "valu
 class other_error : public exception
 {
   public:
-    static other_error create(int id_, const std::string& what_arg)
+    static other_error create(int id, const std::string& what_arg)
     {
-        std::string w = exception::name("other_error", id_) + what_arg;
-        return other_error(id_, w.c_str());
+        std::string w = exception::name("other_error", id) + what_arg;
+        return other_error(id, w.c_str());
     }
 
   private:
-    other_error(int id_, const char* what_arg)
-        : exception(id_, what_arg)
+    other_error(int id, const char* what_arg) noexcept(noexcept(std::runtime_error(what_arg)))
+        : exception(id, what_arg)
     {}
 };
 
diff --git a/test/src/unit-noexcept.cpp b/test/src/unit-noexcept.cpp
index f2fbc8e6..8269574e 100644
--- a/test/src/unit-noexcept.cpp
+++ b/test/src/unit-noexcept.cpp
@@ -62,15 +62,16 @@ TEST_CASE("runtime checks")
 {
     SECTION("nothrow-copy-constructible exceptions")
     {
-        // for ERR60-CPP (https://github.com/nlohmann/json/issues/531)
-        if (std::is_nothrow_copy_constructible<std::runtime_error>::value)
-        {
-            CHECK(std::is_nothrow_copy_constructible<json::exception>::value);
-            CHECK(std::is_nothrow_copy_constructible<json::parse_error>::value);
-            CHECK(std::is_nothrow_copy_constructible<json::invalid_iterator>::value);
-            CHECK(std::is_nothrow_copy_constructible<json::type_error>::value);
-            CHECK(std::is_nothrow_copy_constructible<json::out_of_range>::value);
-            CHECK(std::is_nothrow_copy_constructible<json::other_error>::value);
-        }
+        // for ERR60-CPP (https://github.com/nlohmann/json/issues/531):
+        // Exceptions should be nothrow-copy-constructible. However, compilers
+        // treat std::runtime_exception differently in this regard. Therefore,
+        // we can only demand nothrow-copy-constructibility for our exceptions
+        // if std::runtime_exception is.
+        CHECK(std::is_nothrow_copy_constructible<json::exception>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
+        CHECK(std::is_nothrow_copy_constructible<json::parse_error>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
+        CHECK(std::is_nothrow_copy_constructible<json::invalid_iterator>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
+        CHECK(std::is_nothrow_copy_constructible<json::type_error>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
+        CHECK(std::is_nothrow_copy_constructible<json::out_of_range>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
+        CHECK(std::is_nothrow_copy_constructible<json::other_error>::value == std::is_nothrow_copy_constructible<std::runtime_error>::value);
     }
 }