diff --git a/src/lib/pulse/Error.cxx b/src/lib/pulse/Error.cxx
index cac73510a..94efaa399 100644
--- a/src/lib/pulse/Error.cxx
+++ b/src/lib/pulse/Error.cxx
@@ -23,9 +23,20 @@
 #include <pulse/context.h>
 #include <pulse/error.h>
 
-std::runtime_error
-MakePulseError(pa_context *context, const char *prefix) noexcept
+namespace Pulse {
+
+std::string
+ErrorCategory::message(int condition) const
 {
-	const int e = pa_context_errno(context);
-	return FormatRuntimeError("%s: %s", prefix, pa_strerror(e));
+	return pa_strerror(condition);
 }
+
+ErrorCategory error_category;
+
+std::system_error
+MakeError(pa_context *context, const char *msg) noexcept
+{
+	return MakeError(pa_context_errno(context), msg);
+}
+
+} // namespace Pulse
diff --git a/src/lib/pulse/Error.hxx b/src/lib/pulse/Error.hxx
index d7243bafa..89a173e2f 100644
--- a/src/lib/pulse/Error.hxx
+++ b/src/lib/pulse/Error.hxx
@@ -17,14 +17,34 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
-#ifndef MPD_PULSE_ERROR_HXX
-#define MPD_PULSE_ERROR_HXX
+#pragma once
 
-#include <stdexcept>
+#include <system_error>
 
 struct pa_context;
 
-std::runtime_error
-MakePulseError(pa_context *context, const char *prefix) noexcept;
+namespace Pulse {
 
-#endif
+class ErrorCategory final : public std::error_category {
+public:
+	const char *name() const noexcept override {
+		return "pulse";
+	}
+
+	std::string message(int condition) const override;
+};
+
+extern ErrorCategory error_category;
+
+[[nodiscard]] [[gnu::pure]]
+inline std::system_error
+MakeError(int error, const char *msg) noexcept
+{
+	return std::system_error(error, error_category, msg);
+}
+
+[[nodiscard]] [[gnu::pure]]
+std::system_error
+MakeError(pa_context *context, const char *msg) noexcept;
+
+} // namespace Pulse
diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx
index c2ece7136..6be79df20 100644
--- a/src/output/plugins/PulseOutputPlugin.cxx
+++ b/src/output/plugins/PulseOutputPlugin.cxx
@@ -368,8 +368,8 @@ PulseOutput::Connect()
 
 	if (pa_context_connect(context, server,
 			       (pa_context_flags_t)0, nullptr) < 0)
-		throw MakePulseError(context,
-				     "pa_context_connect() has failed");
+		throw Pulse::MakeError(context,
+				       "pa_context_connect() has failed");
 }
 
 void
@@ -501,8 +501,8 @@ PulseOutput::WaitConnection()
 		case PA_CONTEXT_FAILED:
 			/* failure */
 			{
-				auto e = MakePulseError(context,
-							"failed to connect");
+				auto e = Pulse::MakeError(context,
+							  "failed to connect");
 				DeleteContext();
 				throw e;
 			}
@@ -603,8 +603,8 @@ PulseOutput::SetupStream(const pa_sample_spec &ss)
 				   PA_CHANNEL_MAP_WAVEEX);
 	stream = pa_stream_new(context, name, &ss, &chan_map);
 	if (stream == nullptr)
-		throw MakePulseError(context,
-				     "pa_stream_new() has failed");
+		throw Pulse::MakeError(context,
+				       "pa_stream_new() has failed");
 
 	pa_stream_set_suspended_callback(stream,
 					 pulse_output_stream_suspended_cb,
@@ -682,8 +682,8 @@ PulseOutput::Open(AudioFormat &audio_format)
 				       nullptr, nullptr) < 0) {
 		DeleteStream();
 
-		throw MakePulseError(context,
-				     "pa_stream_connect_playback() has failed");
+		throw Pulse::MakeError(context,
+				       "pa_stream_connect_playback() has failed");
 	}
 
 	interrupted = false;
@@ -729,8 +729,8 @@ PulseOutput::WaitStream()
 		case PA_STREAM_FAILED:
 		case PA_STREAM_TERMINATED:
 		case PA_STREAM_UNCONNECTED:
-			throw MakePulseError(context,
-					     "failed to connect the stream");
+			throw Pulse::MakeError(context,
+					       "failed to connect the stream");
 
 		case PA_STREAM_CREATING:
 			if (interrupted)
@@ -752,12 +752,12 @@ PulseOutput::StreamPause(bool _pause)
 	pa_operation *o = pa_stream_cork(stream, _pause,
 					 pulse_output_stream_success_cb, this);
 	if (o == nullptr)
-		throw MakePulseError(context,
-				     "pa_stream_cork() has failed");
+		throw Pulse::MakeError(context,
+				       "pa_stream_cork() has failed");
 
 	if (!pulse_wait_for_operation(mainloop, o))
-		throw MakePulseError(context,
-				     "pa_stream_cork() has failed");
+		throw Pulse::MakeError(context,
+				       "pa_stream_cork() has failed");
 }
 
 std::chrono::steady_clock::duration
@@ -819,7 +819,7 @@ PulseOutput::Play(std::span<const std::byte> src)
 	int result = pa_stream_write(stream, src.data(), src.size(), nullptr,
 				     0, PA_SEEK_RELATIVE);
 	if (result < 0)
-		throw MakePulseError(context, "pa_stream_write() failed");
+		throw Pulse::MakeError(context, "pa_stream_write() failed");
 
 	return src.size();
 }
@@ -838,7 +838,7 @@ PulseOutput::Drain()
 		pa_stream_drain(stream,
 				pulse_output_stream_success_cb, this);
 	if (o == nullptr)
-		throw MakePulseError(context, "pa_stream_drain() failed");
+		throw Pulse::MakeError(context, "pa_stream_drain() failed");
 
 	pulse_wait_for_operation(mainloop, o);
 }