diff --git a/sup/include/sup/scope_exit.hpp b/sup/include/sup/scope_exit.hpp
index 67faecebd47974f47c484dc4a7231c92fa0343ac..acba9da5a96d5f842f52f1dbe577a35c7ee151b4 100644
--- a/sup/include/sup/scope_exit.hpp
+++ b/sup/include/sup/scope_exit.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <functional>
 #include <type_traits>
 #include <utility>
 
@@ -42,9 +43,34 @@ public:
      }
 };
 
+// std::function is not nothrow move constructable before C++20, so, er, cheat.
+namespace impl {
+    template <typename R>
+    struct wrap_std_function {
+        std::function<R ()> f;
+
+        wrap_std_function() noexcept {}
+        wrap_std_function(const std::function<R ()>& f): f(f) {}
+        wrap_std_function(std::function<R ()>&& f): f(std::move(f)) {}
+        wrap_std_function(wrap_std_function&& other) noexcept {
+            try {
+                f = std::move(other.f);
+            }
+            catch (...) {}
+        }
+
+        void operator()() const { f(); }
+    };
+}
+
 template <typename F>
-scope_exit<std::decay_t<F>> on_scope_exit(F&& f) {
+auto on_scope_exit(F&& f) {
     return scope_exit<std::decay_t<F>>(std::forward<F>(f));
 }
 
+template <typename R>
+auto on_scope_exit(std::function<R ()> f) {
+    return on_scope_exit(impl::wrap_std_function<R>(std::move(f)));
+}
+
 } // namespace sup
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index c78bf48c894c1d3341a13f835e6602ca03222588..b89cece7d93a51260b005155aa18cd37948d85aa 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -94,6 +94,7 @@ set(unit_sources
     test_schedule.cpp
     test_spike_source.cpp
     test_local_context.cpp
+    test_scope_exit.cpp
     test_simd.cpp
     test_span.cpp
     test_spikes.cpp
diff --git a/test/unit/test_scope_exit.cpp b/test/unit/test_scope_exit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..12b8c140aed5dbb1f32f58ec202e2c9ed97157ad
--- /dev/null
+++ b/test/unit/test_scope_exit.cpp
@@ -0,0 +1,49 @@
+#include <functional>
+
+#include "../gtest.h"
+
+#include <sup/scope_exit.hpp>
+
+using sup::on_scope_exit;
+
+TEST(scope_exit, basic) {
+    bool a = false;
+    {
+        auto guard = on_scope_exit([&a] { a = true; });
+        EXPECT_FALSE(a);
+    }
+    EXPECT_TRUE(a);
+}
+
+TEST(scope_exit, noexceptcall) {
+    auto guard1 = on_scope_exit([] {});
+    using G1 = decltype(guard1);
+    EXPECT_FALSE(noexcept(guard1.~G1()));
+
+    auto guard2 = on_scope_exit([]() noexcept {});
+    using G2 = decltype(guard2);
+    EXPECT_TRUE(noexcept(guard2.~G2()));
+}
+
+TEST(scope_exit, function) {
+    // on_scope_exit has a special overload for std::function
+    // to work around its non-noexcept move ctor.
+    bool a = false;
+    std::function<void ()> setter = [&a] { a = true; };
+
+    {
+        auto guard = on_scope_exit(setter);
+        EXPECT_FALSE(a);
+    }
+    EXPECT_TRUE(a);
+
+    a = false;
+    std::function<int ()> setter2 = [&a] { a = true; return 3; };
+
+    {
+        auto guard = on_scope_exit(setter2);
+        EXPECT_FALSE(a);
+    }
+    EXPECT_TRUE(a);
+}
+