diff --git a/src/c-stdaux-assert.h b/src/c-stdaux-assert.h
new file mode 100644
index 0000000..a878e80
--- /dev/null
+++ b/src/c-stdaux-assert.h
@@ -0,0 +1,194 @@
+/* There is no include guard. Just like <assert.h>, we can include this header
+ * multiple times to update the macros for NDEBUG/C_MORE_ASSERT changes.
+ *
+ * The user can define NDEBUG to disable all asserts.
+ *
+ * The user can define C_MORE_ASSERT to a non-negative number to control
+ * which assertions are enabled.
+ */
+
+#include <assert.h>
+#include <c-stdaux-generic.h>
+
+/**
+ * C_MORE_ASSERT: user define to configure assertion levels (similar to NDEBUG).
+ *
+ * If NDEBUG is defined, then assert() is a nop. This also implies
+ * C_MORE_ASSERT_LEVEL of zero, which means that c_more_assert() does not
+ * evaluate the condition at runtime.
+ *
+ * Otherwise, if C_MORE_ASSERT is defined it determines the
+ * C_MORE_ASSERT_LEVEL. If C_MORE_ASSERT, is undefined, C_MORE_ASSERT_LEVEL
+ * defaults to 1.
+ *
+ * The effective C_MORE_ASSERT_LEVEL affects whether c_more_assert() and
+ * c_more_assert_with() evaluates the condition at runtime. The purpose is that
+ * more assertions are disabled by default (and in release builds). For
+ * debugging and testing, define C_MORE_ASSERT to a number larger than 1.
+ */
+#undef C_MORE_ASSERT_LEVEL
+#ifdef NDEBUG
+#define C_MORE_ASSERT_LEVEL 0
+#elif !defined(C_MORE_ASSERT)
+#define C_MORE_ASSERT_LEVEL 1
+#else
+#define C_MORE_ASSERT_LEVEL (C_MORE_ASSERT)
+#endif
+
+#undef _c_assert_fail
+#if C_MORE_ASSERT_LEVEL > 0 && defined(__GNU_LIBRARY__)
+/* Depending on "_with_msg", we hide the "msg" string unless we build with
+ * "C_MORE_ASSERT > 1". The point is to avoid embedding debugging strings in
+ * the binary with release builds.
+ *
+ * The assertion failure messages are often not very useful for the end user
+ * and for the developer __FILE__:__LINE__ is sufficient.
+ *
+ * __assert_fail() also exists on musl, but we don't have a separate detection
+ * for musl.
+ */
+#define _c_assert_fail(_with_msg, msg)                                       \
+    __assert_fail(                                                           \
+        C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "" msg "" : "<dropped>",    \
+        __FILE__, __LINE__,                                                  \
+        C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "<unknown-fcn>" : __func__)
+#else
+#define _c_assert_fail(_with_msg, msg) \
+    do {                               \
+        assert(false && msg);          \
+        _c_unreachable_code();         \
+    } while (0)
+#endif
+
+/* There is an include guard. The remainder of this header is only evaluated
+ * once upon multiple inclusions. */
+#if !defined(C_HAS_STDAUX_ASSERT)
+#define C_HAS_STDAUX_ASSERT
+
+#if defined(C_COMPILER_GNUC)
+#define _c_unreachable_code() __builtin_unreachable()
+#else /* defined(C_COMPILER_GNUC) */
+#define _c_unreachable_code()                                                 \
+    do {                                                                      \
+        /* Infinite loop without side effects is undefined behavior and marks \
+         * unreachable code. */                                               \
+    } while (1)
+#endif /* defined(C_COMPILER_GNUC) */
+
+#if defined(C_COMPILER_GNUC)
+#define _c_assert_constant(_cond)                                            \
+    do {                                                                     \
+        if (__builtin_constant_p(_cond) && !(_cond)) {                       \
+            /* With gcc, constant expressions are still evaluated and result \
+             * in unreachable code too.                                      \
+             *                                                               \
+             * The point is to avoid compiler warnings with                  \
+             * c_more_assert(false) and NDEBUG.                              \
+             */                                                              \
+            _c_unreachable_code();                                           \
+        }                                                                    \
+    } while (0)
+#else /* defined(C_COMPILER_GNUC) */
+#define _c_assert_constant(_cond) \
+    do {                          \
+        /* This does nothing. */  \
+    } while (0)
+#endif /* defined(C_COMPILER_GNUC) */
+
+/**
+ * c_more_assert_with() - Conditional runtime assertion.
+ * @_level: Assertion level that determines whether the assertion is evaluated,
+ *     based on comparison with C_MORE_ASSERT_LEVEL.
+ * @_cond: Condition or expression to validate.
+ *
+ * This macro performs an assertion based on the specified _level in comparison
+ * to the compile-time constant C_MORE_ASSERT_LEVEL. C_MORE_ASSERT_LEVEL
+ * typically defaults to 1 but can be modified by defining NDEBUG or
+ * C_MORE_ASSERT.
+ *
+ * - If _level is less than C_MORE_ASSERT_LEVEL, the condition is ignored and
+ *   the assertion code is excluded from the final build, allowing for performance
+ *   optimizations.
+ *
+ * - If _cond is a constant expression that fails, the compiler will mark the code
+ *   path as unreachable, regardless of NDEBUG or the configured C_MORE_ASSERT_LEVEL.
+ *
+ * Unlike c_assert(), which always evaluates the condition,
+ * `c_more_assert_with()` * only evaluates the condition if the specified _level *
+ * meets the configured assertion threshold. This conditional behavior requires *
+ * that _cond has no side effects, as it may not be evaluated in all cases.
+ *
+ * Note: This macro is usually excluded from regular builds unless explicitly
+ * enabled by defining C_MORE_ASSERT, making it particularly useful for debugging
+ * and testing without incurring runtime costs in production builds.
+ *
+ * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
+ */
+#define c_more_assert_with(_level, _cond)                        \
+    do {                                                         \
+        /* c_more_assert_with() must do *nothing* of effect,     \
+         * except evaluating @_cond (0 or 1 times).              \
+         *                                                       \
+         * As such, it is async-signal-safe (provided @_cond and \
+         * @_level is, and the assertion does not fail). */      \
+        if ((_level) < C_MORE_ASSERT_LEVEL) {                    \
+            _c_assert_constant(_cond);                           \
+        } else if (_c_likely_(_cond)) {                          \
+            /* pass */                                           \
+        } else {                                                 \
+            _c_assert_fail(false, #_cond);                       \
+        }                                                        \
+    } while (0)
+
+/**
+ * c_more_assert() - Conditional runtime assertion.
+ * @_cond: Condition or expression to validate.
+ *
+ * This is the same as c_more_assert_with(2, _cond). This means that
+ * the assertion is usually disabled in regular builds unless the user
+ * opts in by setting C_MORE_ASSERT to 2 or larger.
+ *
+ * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
+ */
+#define c_more_assert(_cond) c_more_assert_with(2, _cond)
+
+/**
+ * c_assert() - Runtime assertions
+ * @_cond:                 Result of an expression
+ *
+ * This function behaves like the standard ``assert(3)`` macro. That is, if
+ * ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that
+ * the result of the passed expression is true.
+ *
+ * Unlike the standard ``assert(3)`` macro, this function always evaluates its
+ * argument. This means side-effects will always be evaluated! However, if the
+ * macro is used with constant expressions, the compiler will be able to
+ * optimize it away.
+ *
+ * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
+ */
+#define c_assert(_cond)                    \
+    do {                                   \
+        if (!_c_likely_(_cond)) {          \
+            _c_assert_fail(false, #_cond); \
+        }                                  \
+    } while (0)
+
+/**
+ * c_assert_not_reached() - Fail assertion when called.
+ *
+ * With C_COMPILER_GNUC, the macro calls assert(false) and marks the code
+ * path as __builtin_unreachable(). The benefit is that also with NDEBUG the
+ * compiler considers the path unreachable.
+ *
+ * Otherwise, just calls assert(false).
+ *
+ * The macro is async-signal-safe.
+ */
+#define c_assert_not_reached() _c_assert_fail(true, "unreachable")
+
+#endif /* !defined(C_HAS_STDAUX_ASSERT) */
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/c-stdaux-generic.h b/src/c-stdaux-generic.h
index 6f87fd4..3800b8c 100644
--- a/src/c-stdaux-generic.h
+++ b/src/c-stdaux-generic.h
@@ -81,7 +81,6 @@ extern "C" {
  */
 /**/
 
-#include <assert.h>
 #include <errno.h>
 #include <inttypes.h>
 #include <limits.h>
@@ -129,6 +128,8 @@ extern "C" {
  *
  * Outside of macros, this has no added value.
  *
+ * This macro is async-signal-safe, provided that the condition is.
+ *
  * Return: Evaluates to the value of ``!!_x``.
  */
 #define _c_boolean_expr_(_x) _c_internal_boolean_expr_(__COUNTER__, _x)
@@ -164,6 +165,8 @@ extern "C" {
  *
  * Alias for ``__builtin_expect(!!(_x), 1)``.
  *
+ * This macro is async-signal-safe, provided that the condition is.
+ *
  * Return: The expression ``!!_x`` is evaluated and returned.
  */
 #define _c_likely_(_x) _c_internal_likely_(_x)
@@ -200,6 +203,8 @@ extern "C" {
  *
  * Alias for ``__builtin_expect(!!(_x), 0)``.
  *
+ * This macro is async-signal-safe, provided that the condition is.
+ *
  * Return: The expression ``!!_x`` is evaluated and returned.
  */
 #define _c_unlikely_(_x) _c_internal_unlikely_(_x)
@@ -310,25 +315,6 @@ extern "C" {
 #  define c_internal_assume_aligned(_ptr, _alignment, _offset) ((void)(_alignment), (void)(_offset), (_ptr))
 #endif
 
-/**
- * c_assert() - Runtime assertions
- * @_x:                 Result of an expression
- *
- * This function behaves like the standard ``assert(3)`` macro. That is, if
- * ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that
- * the result of the passed expression is true.
- *
- * Unlike the standard ``assert(3)`` macro, this function always evaluates its
- * argument. This means side-effects will always be evaluated! However, if the
- * macro is used with constant expressions, the compiler will be able to
- * optimize it away.
- */
-#define c_assert(_x) (                                                          \
-                _c_likely_(_x)                                                  \
-                        ? assert(true && #_x)                                   \
-                        : assert(false && #_x)                                  \
-        )
-
 /**
  * c_errno() - Return valid errno
  *
diff --git a/src/c-stdaux.h b/src/c-stdaux.h
index 0eccb20..23be069 100644
--- a/src/c-stdaux.h
+++ b/src/c-stdaux.h
@@ -50,6 +50,8 @@ extern "C" {
 #  include <c-stdaux-unix.h>
 #endif
 
+#include <c-stdaux-assert.h>
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/docs/api.rst b/src/docs/api.rst
index a25e9fa..be55c53 100644
--- a/src/docs/api.rst
+++ b/src/docs/api.rst
@@ -1,5 +1,5 @@
 API
 ===
 
-.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h
+.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h c-stdaux-assert.h
    :transform: kerneldoc
diff --git a/src/meson.build b/src/meson.build
index 0e958af..16dbd96 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -20,6 +20,7 @@ libcstdaux_dep = declare_dependency(
 if not meson.is_subproject()
         install_headers(
                 'c-stdaux.h',
+                'c-stdaux-assert.h',
                 'c-stdaux-generic.h',
                 'c-stdaux-gnuc.h',
                 'c-stdaux-unix.h',
diff --git a/src/test-api.c b/src/test-api.c
index e52d42c..1dab1a5 100644
--- a/src/test-api.c
+++ b/src/test-api.c
@@ -20,6 +20,8 @@ static void direct_cleanup_fn(int p) { (void)p; }
 C_DEFINE_CLEANUP(int, cleanup_fn);
 C_DEFINE_DIRECT_CLEANUP(int, direct_cleanup_fn);
 
+int global_int_0;
+
 static void test_api_generic(void) {
         /* C_COMPILER_* */
         {
@@ -155,6 +157,34 @@ static void test_api_generic(void) {
                 for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i)
                         c_assert(!!fns[i]);
         }
+
+        if (false)
+                c_assert_not_reached();
+
+        switch (global_int_0) {
+        default:
+                /* Test that we don't get a -Wimplicit-fallthrough warning and
+                 * the compiler detect that the function doesn't return. */
+                c_assert_not_reached();
+        case 1:
+        case 0:
+                c_assert(global_int_0 == 0);
+                break;
+        }
+
+        {
+                int v;
+
+                v = 0;
+                c_assert((v = 1));
+                c_assert(v == 1);
+
+                v = 5;
+                c_more_assert_with(C_MORE_ASSERT_LEVEL - 1, (++v == -1));
+                c_more_assert_with(C_MORE_ASSERT_LEVEL, (++v == 6));
+                c_more_assert_with(C_MORE_ASSERT_LEVEL + 1, (++v == 7));
+                c_assert(v == 7);
+        }
 }
 
 #else /* C_MODULE_GENERIC */
@@ -280,6 +310,16 @@ static void test_api_gnuc(void) {
         {
                 c_assert(c_align_to(0, 0) == 0);
         }
+
+        /* Check that assertions can be nested without compiler warnings. That easily
+         * happens when asserting on macros that contain expression statements and
+         * themselves assertions. */
+        {
+            c_assert(__extension__({ c_assert(true); true; }));
+            c_assert(__extension__({ c_more_assert(true); true; }));
+            c_more_assert(__extension__({ c_assert(true); true; }));
+            c_more_assert(__extension__({ c_more_assert(true); true; }));
+        }
 }
 
 #else /* C_MODULE_GNUC */