diff --git a/src/TagArchive.cxx b/src/TagArchive.cxx
index a5a849d7b..dd1b73a38 100644
--- a/src/TagArchive.cxx
+++ b/src/TagArchive.cxx
@@ -28,26 +28,28 @@
 bool
 tag_archive_scan(ArchiveFile &archive, const char *path_utf8,
 		 const TagHandler &handler, void *handler_ctx)
-{
+try {
 	Mutex mutex;
 	Cond cond;
 
-	InputStreamPtr is(archive.OpenStream(path_utf8, mutex, cond,
-					     IgnoreError()));
+	InputStreamPtr is(archive.OpenStream(path_utf8, mutex, cond));
 	if (!is)
 		return false;
 
 	return tag_stream_scan(*is, handler, handler_ctx);
+} catch (const std::exception &e) {
+	return false;
 }
 
 bool
 tag_archive_scan(ArchiveFile &archive, const char *path_utf8,
 		 TagBuilder &builder)
-{
+try {
 	Mutex mutex;
 	Cond cond;
 
-	InputStreamPtr is(archive.OpenStream(path_utf8, mutex, cond,
-					     IgnoreError()));
+	InputStreamPtr is(archive.OpenStream(path_utf8, mutex, cond));
 	return is && tag_stream_scan(*is, builder);
+} catch (const std::exception &e) {
+	return false;
 }
diff --git a/src/TagFile.cxx b/src/TagFile.cxx
index 56e235462..8c6ccb61a 100644
--- a/src/TagFile.cxx
+++ b/src/TagFile.cxx
@@ -30,6 +30,8 @@
 #include "input/LocalOpen.hxx"
 #include "thread/Cond.hxx"
 
+#include <stdexcept>
+
 #include <assert.h>
 
 class TagFileScan {
@@ -60,11 +62,12 @@ public:
 
 		/* open the InputStream (if not already open) */
 		if (is == nullptr) {
-			is = OpenLocalInputStream(path_fs,
-						  mutex, cond,
-						  IgnoreError());
-			if (is == nullptr)
+			try {
+				is = OpenLocalInputStream(path_fs,
+							  mutex, cond);
+			} catch (const std::runtime_error &) {
 				return false;
+			}
 		} else
 			is->LockRewind(IgnoreError());
 
diff --git a/src/TagStream.cxx b/src/TagStream.cxx
index 1ffeac128..45b18e60f 100644
--- a/src/TagStream.cxx
+++ b/src/TagStream.cxx
@@ -72,13 +72,14 @@ tag_stream_scan(InputStream &is, const TagHandler &handler, void *ctx)
 
 bool
 tag_stream_scan(const char *uri, const TagHandler &handler, void *ctx)
-{
+try {
 	Mutex mutex;
 	Cond cond;
 
-	auto is = InputStream::OpenReady(uri, mutex, cond,
-					 IgnoreError());
-	return is && tag_stream_scan(*is, handler, ctx);
+	auto is = InputStream::OpenReady(uri, mutex, cond);
+	return tag_stream_scan(*is, handler, ctx);
+} catch (const std::exception &e) {
+	return false;
 }
 
 bool
@@ -97,11 +98,12 @@ tag_stream_scan(InputStream &is, TagBuilder &builder)
 
 bool
 tag_stream_scan(const char *uri, TagBuilder &builder)
-{
+try {
 	Mutex mutex;
 	Cond cond;
 
-	auto is = InputStream::OpenReady(uri, mutex, cond,
-					 IgnoreError());
-	return is && tag_stream_scan(*is, builder);
+	auto is = InputStream::OpenReady(uri, mutex, cond);
+	return tag_stream_scan(*is, builder);
+} catch (const std::exception &e) {
+	return false;
 }
diff --git a/src/archive/ArchiveFile.hxx b/src/archive/ArchiveFile.hxx
index 3e9e7a208..b7335072c 100644
--- a/src/archive/ArchiveFile.hxx
+++ b/src/archive/ArchiveFile.hxx
@@ -51,11 +51,12 @@ public:
 	/**
 	 * Opens an InputStream of a file within the archive.
 	 *
+	 * Throws std::runtime_error on error.
+	 *
 	 * @param path the path within the archive
 	 */
 	virtual InputStream *OpenStream(const char *path,
-					Mutex &mutex, Cond &cond,
-					Error &error) = 0;
+					Mutex &mutex, Cond &cond) = 0;
 };
 
 #endif
diff --git a/src/archive/plugins/Bzip2ArchivePlugin.cxx b/src/archive/plugins/Bzip2ArchivePlugin.cxx
index 63ff2841c..1601b8c42 100644
--- a/src/archive/plugins/Bzip2ArchivePlugin.cxx
+++ b/src/archive/plugins/Bzip2ArchivePlugin.cxx
@@ -36,6 +36,8 @@
 
 #include <bzlib.h>
 
+#include <stdexcept>
+
 #include <stddef.h>
 
 class Bzip2ArchiveFile final : public ArchiveFile {
@@ -74,9 +76,8 @@ public:
 		visitor.VisitArchiveEntry(name.c_str());
 	}
 
-	virtual InputStream *OpenStream(const char *path,
-					Mutex &mutex, Cond &cond,
-					Error &error) override;
+	InputStream *OpenStream(const char *path,
+				Mutex &mutex, Cond &cond) override;
 };
 
 class Bzip2InputStream final : public InputStream {
@@ -93,13 +94,12 @@ public:
 			 Mutex &mutex, Cond &cond);
 	~Bzip2InputStream();
 
-	bool Open(Error &error);
-
 	/* virtual methods from InputStream */
 	bool IsEOF() override;
 	size_t Read(void *ptr, size_t size, Error &error) override;
 
 private:
+	void Open();
 	bool FillBuffer(Error &error);
 };
 
@@ -107,8 +107,8 @@ static constexpr Domain bz2_domain("bz2");
 
 /* single archive handling allocation helpers */
 
-inline bool
-Bzip2InputStream::Open(Error &error)
+inline void
+Bzip2InputStream::Open()
 {
 	bzstream.bzalloc = nullptr;
 	bzstream.bzfree = nullptr;
@@ -118,27 +118,20 @@ Bzip2InputStream::Open(Error &error)
 	bzstream.avail_in = 0;
 
 	int ret = BZ2_bzDecompressInit(&bzstream, 0, 0);
-	if (ret != BZ_OK) {
-		error.Set(bz2_domain, ret,
-			  "BZ2_bzDecompressInit() has failed");
-		return false;
-	}
+	if (ret != BZ_OK)
+		throw std::runtime_error("BZ2_bzDecompressInit() has failed");
 
 	SetReady();
-	return true;
 }
 
 /* archive open && listing routine */
 
 static ArchiveFile *
-bz2_open(Path pathname, Error &error)
+bz2_open(Path pathname, gcc_unused Error &error)
 {
 	static Mutex mutex;
 	static Cond cond;
-	auto is = OpenLocalInputStream(pathname, mutex, cond, error);
-	if (is == nullptr)
-		return nullptr;
-
+	auto is = OpenLocalInputStream(pathname, mutex, cond);
 	return new Bzip2ArchiveFile(pathname, std::move(is));
 }
 
@@ -150,6 +143,7 @@ Bzip2InputStream::Bzip2InputStream(Bzip2ArchiveFile &_context,
 	:InputStream(_uri, _mutex, _cond),
 	 archive(&_context)
 {
+	Open();
 	archive->Ref();
 }
 
@@ -161,16 +155,9 @@ Bzip2InputStream::~Bzip2InputStream()
 
 InputStream *
 Bzip2ArchiveFile::OpenStream(const char *path,
-			     Mutex &mutex, Cond &cond,
-			     Error &error)
+			     Mutex &mutex, Cond &cond)
 {
-	Bzip2InputStream *bis = new Bzip2InputStream(*this, path, mutex, cond);
-	if (!bis->Open(error)) {
-		delete bis;
-		return nullptr;
-	}
-
-	return bis;
+	return new Bzip2InputStream(*this, path, mutex, cond);
 }
 
 inline bool
diff --git a/src/archive/plugins/Iso9660ArchivePlugin.cxx b/src/archive/plugins/Iso9660ArchivePlugin.cxx
index d077e3aef..44e3ae71a 100644
--- a/src/archive/plugins/Iso9660ArchivePlugin.cxx
+++ b/src/archive/plugins/Iso9660ArchivePlugin.cxx
@@ -29,6 +29,7 @@
 #include "input/InputStream.hxx"
 #include "fs/Path.hxx"
 #include "util/RefCount.hxx"
+#include "util/RuntimeError.hxx"
 #include "util/Error.hxx"
 #include "util/Domain.hxx"
 
@@ -77,9 +78,8 @@ public:
 
 	virtual void Visit(ArchiveVisitor &visitor) override;
 
-	virtual InputStream *OpenStream(const char *path,
-					Mutex &mutex, Cond &cond,
-					Error &error) override;
+	InputStream *OpenStream(const char *path,
+				Mutex &mutex, Cond &cond) override;
 };
 
 static constexpr Domain iso9660_domain("iso9660");
@@ -175,15 +175,12 @@ public:
 
 InputStream *
 Iso9660ArchiveFile::OpenStream(const char *pathname,
-			       Mutex &mutex, Cond &cond,
-			       Error &error)
+			       Mutex &mutex, Cond &cond)
 {
 	auto statbuf = iso9660_ifs_stat_translate(iso, pathname);
-	if (statbuf == nullptr) {
-		error.Format(iso9660_domain,
-			     "not found in the ISO file: %s", pathname);
-		return nullptr;
-	}
+	if (statbuf == nullptr)
+		throw FormatRuntimeError("not found in the ISO file: %s",
+					 pathname);
 
 	return new Iso9660InputStream(*this, pathname, mutex, cond,
 				      statbuf);
diff --git a/src/archive/plugins/ZzipArchivePlugin.cxx b/src/archive/plugins/ZzipArchivePlugin.cxx
index e83edb09b..45ae02cc3 100644
--- a/src/archive/plugins/ZzipArchivePlugin.cxx
+++ b/src/archive/plugins/ZzipArchivePlugin.cxx
@@ -29,6 +29,7 @@
 #include "input/InputStream.hxx"
 #include "fs/Path.hxx"
 #include "util/RefCount.hxx"
+#include "util/RuntimeError.hxx"
 #include "util/Error.hxx"
 #include "util/Domain.hxx"
 
@@ -58,9 +59,8 @@ public:
 
 	virtual void Visit(ArchiveVisitor &visitor) override;
 
-	virtual InputStream *OpenStream(const char *path,
-					Mutex &mutex, Cond &cond,
-					Error &error) override;
+	InputStream *OpenStream(const char *path,
+				Mutex &mutex, Cond &cond) override;
 };
 
 static constexpr Domain zzip_domain("zzip");
@@ -129,15 +129,12 @@ struct ZzipInputStream final : public InputStream {
 
 InputStream *
 ZzipArchiveFile::OpenStream(const char *pathname,
-			    Mutex &mutex, Cond &cond,
-			    Error &error)
+			    Mutex &mutex, Cond &cond)
 {
 	ZZIP_FILE *_file = zzip_file_open(dir, pathname, 0);
-	if (_file == nullptr) {
-		error.Format(zzip_domain, "not found in the ZIP file: %s",
-			     pathname);
-		return nullptr;
-	}
+	if (_file == nullptr)
+		throw FormatRuntimeError("not found in the ZIP file: %s",
+					 pathname);
 
 	return new ZzipInputStream(*this, pathname,
 				   mutex, cond,
diff --git a/src/decoder/DecoderAPI.cxx b/src/decoder/DecoderAPI.cxx
index cfcb44766..aaba8fd37 100644
--- a/src/decoder/DecoderAPI.cxx
+++ b/src/decoder/DecoderAPI.cxx
@@ -258,7 +258,7 @@ void decoder_seek_error(Decoder & decoder)
 }
 
 InputStreamPtr
-decoder_open_uri(Decoder &decoder, const char *uri, Error &error)
+decoder_open_uri(Decoder &decoder, const char *uri)
 {
 	assert(decoder.dc.state == DecoderState::START ||
 	       decoder.dc.state == DecoderState::DECODE);
@@ -267,9 +267,7 @@ decoder_open_uri(Decoder &decoder, const char *uri, Error &error)
 	Mutex &mutex = dc.mutex;
 	Cond &cond = dc.cond;
 
-	auto is = InputStream::Open(uri, mutex, cond, error);
-	if (!is)
-		return nullptr;
+	auto is = InputStream::Open(uri, mutex, cond);
 
 	const ScopeLock lock(mutex);
 	while (true) {
diff --git a/src/decoder/DecoderAPI.hxx b/src/decoder/DecoderAPI.hxx
index 3ca082a3b..1433e5fd4 100644
--- a/src/decoder/DecoderAPI.hxx
+++ b/src/decoder/DecoderAPI.hxx
@@ -121,11 +121,12 @@ decoder_seek_error(Decoder &decoder);
 
 /**
  * Open a new #InputStream and wait until it's ready.  Can get
- * cancelled by DecoderCommand::STOP (returns nullptr without setting
- * #Error).
+ * cancelled by DecoderCommand::STOP (returns nullptr).
+ *
+ * Throws std::runtime_error on error.
  */
 InputStreamPtr
-decoder_open_uri(Decoder &decoder, const char *uri, Error &error);
+decoder_open_uri(Decoder &decoder, const char *uri);
 
 /**
  * Blocking read from the input stream.
diff --git a/src/decoder/DecoderThread.cxx b/src/decoder/DecoderThread.cxx
index bb8ef13dc..71634724f 100644
--- a/src/decoder/DecoderThread.cxx
+++ b/src/decoder/DecoderThread.cxx
@@ -56,10 +56,7 @@ static constexpr Domain decoder_thread_domain("decoder_thread");
 static InputStreamPtr
 decoder_input_stream_open(DecoderControl &dc, const char *uri)
 {
-	Error error;
-	auto is = InputStream::Open(uri, dc.mutex, dc.cond, error);
-	if (is == nullptr)
-		throw error;
+	auto is = InputStream::Open(uri, dc.mutex, dc.cond);
 
 	/* wait for the input stream to become ready; its metadata
 	   will be available then */
@@ -76,6 +73,7 @@ decoder_input_stream_open(DecoderControl &dc, const char *uri)
 		is->Update();
 	}
 
+	Error error;
 	if (!is->Check(error))
 		throw error;
 
@@ -85,10 +83,7 @@ decoder_input_stream_open(DecoderControl &dc, const char *uri)
 static InputStreamPtr
 decoder_input_stream_open(DecoderControl &dc, Path path)
 {
-	Error error;
-	auto is = OpenLocalInputStream(path, dc.mutex, dc.cond, error);
-	if (is == nullptr)
-		throw error;
+	auto is = OpenLocalInputStream(path, dc.mutex, dc.cond);
 
 	assert(is->IsReady());
 
diff --git a/src/decoder/plugins/WavpackDecoderPlugin.cxx b/src/decoder/plugins/WavpackDecoderPlugin.cxx
index 3a872ce29..8ab06a761 100644
--- a/src/decoder/plugins/WavpackDecoderPlugin.cxx
+++ b/src/decoder/plugins/WavpackDecoderPlugin.cxx
@@ -34,6 +34,8 @@
 
 #include <wavpack/wavpack.h>
 
+#include <stdexcept>
+
 #include <assert.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -490,7 +492,11 @@ wavpack_open_wvc(Decoder &decoder, const char *uri)
 		free(wvc_url);
 	};
 
-	return decoder_open_uri(decoder, uri, IgnoreError());
+	try {
+		return decoder_open_uri(decoder, uri);
+	} catch (const std::runtime_error &) {
+		return nullptr;
+	}
 }
 
 /*
diff --git a/src/input/AsyncInputStream.cxx b/src/input/AsyncInputStream.cxx
index d1445579a..d5092e42e 100644
--- a/src/input/AsyncInputStream.cxx
+++ b/src/input/AsyncInputStream.cxx
@@ -81,6 +81,7 @@ AsyncInputStream::Resume()
 
 	if (paused) {
 		paused = false;
+
 		DoResume();
 	}
 }
@@ -264,7 +265,12 @@ AsyncInputStream::DeferredResume()
 {
 	const ScopeLock protect(mutex);
 
-	Resume();
+	try {
+		Resume();
+	} catch (...) {
+		postponed_exception = std::current_exception();
+		cond.broadcast();
+	}
 }
 
 void
@@ -274,10 +280,16 @@ AsyncInputStream::DeferredSeek()
 	if (seek_state != SeekState::SCHEDULED)
 		return;
 
-	Resume();
+	try {
+		Resume();
 
-	seek_state = SeekState::PENDING;
-	buffer.Clear();
-	paused = false;
-	DoSeek(seek_offset);
+		seek_state = SeekState::PENDING;
+		buffer.Clear();
+		paused = false;
+
+		DoSeek(seek_offset);
+	} catch (...) {
+		postponed_exception = std::current_exception();
+		cond.broadcast();
+	}
 }
diff --git a/src/input/InputPlugin.hxx b/src/input/InputPlugin.hxx
index 08a9d42fd..d33161c93 100644
--- a/src/input/InputPlugin.hxx
+++ b/src/input/InputPlugin.hxx
@@ -36,7 +36,6 @@
 
 struct ConfigBlock;
 class InputStream;
-class Error;
 struct Tag;
 
 struct InputPlugin {
@@ -58,9 +57,11 @@ struct InputPlugin {
 	 */
 	void (*finish)();
 
+	/**
+	 * Throws std::runtime_error on error.
+	 */
 	InputStream *(*open)(const char *uri,
-			     Mutex &mutex, Cond &cond,
-			     Error &error);
+			     Mutex &mutex, Cond &cond);
 };
 
 #endif
diff --git a/src/input/InputStream.hxx b/src/input/InputStream.hxx
index aabbc8ef5..d93784991 100644
--- a/src/input/InputStream.hxx
+++ b/src/input/InputStream.hxx
@@ -115,16 +115,17 @@ public:
 	 * Opens a new input stream.  You may not access it until the "ready"
 	 * flag is set.
 	 *
+	 * Throws std::runtime_error on error.
+	 *
 	 * @param mutex a mutex that is used to protect this object; must be
 	 * locked before calling any of the public methods
 	 * @param cond a cond that gets signalled when the state of
 	 * this object changes; may be nullptr if the caller doesn't want to get
 	 * notifications
-	 * @return an #InputStream object on success, nullptr on error
+	 * @return an #InputStream object on success
 	 */
 	gcc_nonnull_all
-	static InputStreamPtr Open(const char *uri, Mutex &mutex, Cond &cond,
-				   Error &error);
+	static InputStreamPtr Open(const char *uri, Mutex &mutex, Cond &cond);
 
 	/**
 	 * Just like Open(), but waits for the stream to become ready.
@@ -132,8 +133,7 @@ public:
 	 */
 	gcc_nonnull_all
 	static InputStreamPtr OpenReady(const char *uri,
-					Mutex &mutex, Cond &cond,
-					Error &error);
+					Mutex &mutex, Cond &cond);
 
 	/**
 	 * The absolute URI which was used to open this stream.
diff --git a/src/input/LocalOpen.cxx b/src/input/LocalOpen.cxx
index 58bf57ee2..f5b66e96d 100644
--- a/src/input/LocalOpen.cxx
+++ b/src/input/LocalOpen.cxx
@@ -27,7 +27,7 @@
 #endif
 
 #include "fs/Path.hxx"
-#include "util/Error.hxx"
+#include "system/Error.hxx"
 
 #include <assert.h>
 
@@ -36,20 +36,24 @@
 #endif
 
 InputStreamPtr
-OpenLocalInputStream(Path path, Mutex &mutex, Cond &cond, Error &error)
+OpenLocalInputStream(Path path, Mutex &mutex, Cond &cond)
 {
-	assert(!error.IsDefined());
+	InputStreamPtr is;
 
-	InputStreamPtr is(OpenFileInputStream(path, mutex, cond, error));
 #ifdef ENABLE_ARCHIVE
-	if (is == nullptr && error.IsDomain(errno_domain) &&
-	    error.GetCode() == ENOTDIR) {
-		/* ENOTDIR means this may be a path inside an archive
-		   file */
-		Error error2;
-		is.reset(OpenArchiveInputStream(path, mutex, cond, error2));
-		if (is == nullptr && error2.IsDefined())
-			error = std::move(error2);
+	try {
+#endif
+		is = OpenFileInputStream(path, mutex, cond);
+#ifdef ENABLE_ARCHIVE
+	} catch (const std::system_error &e) {
+		if (IsPathNotFound(e)) {
+			/* ENOTDIR means this may be a path inside an archive
+			   file */
+			is = OpenArchiveInputStream(path, mutex, cond);
+			if (!is)
+				throw;
+		} else
+			throw;
 	}
 #endif
 
diff --git a/src/input/LocalOpen.hxx b/src/input/LocalOpen.hxx
index 787c644b3..3b8e95997 100644
--- a/src/input/LocalOpen.hxx
+++ b/src/input/LocalOpen.hxx
@@ -26,13 +26,14 @@
 class Path;
 class Mutex;
 class Cond;
-class Error;
 
 /**
  * Open a "local" file.  This is a wrapper for the input plugins
  * "file" and "archive".
+ *
+ * Throws std::runtime_error on error.
  */
 InputStreamPtr
-OpenLocalInputStream(Path path, Mutex &mutex, Cond &cond, Error &error);
+OpenLocalInputStream(Path path, Mutex &mutex, Cond &cond);
 
 #endif
diff --git a/src/input/Open.cxx b/src/input/Open.cxx
index 9d52541bb..53afb5a7b 100644
--- a/src/input/Open.cxx
+++ b/src/input/Open.cxx
@@ -28,51 +28,51 @@
 #include "fs/AllocatedPath.hxx"
 #include "util/Error.hxx"
 
+#include <stdexcept>
+
 InputStreamPtr
 InputStream::Open(const char *url,
-		  Mutex &mutex, Cond &cond,
-		  Error &error)
+		  Mutex &mutex, Cond &cond)
 {
 	if (PathTraitsUTF8::IsAbsolute(url)) {
+		Error error;
 		const auto path = AllocatedPath::FromUTF8(url, error);
 		if (path.IsNull())
-			return nullptr;
+			throw std::runtime_error(error.GetMessage());
 
-		return OpenLocalInputStream(path,
-					    mutex, cond, error);
+		return OpenLocalInputStream(path, mutex, cond);
 	}
 
 	input_plugins_for_each_enabled(plugin) {
 		InputStream *is;
 
-		is = plugin->open(url, mutex, cond, error);
+		is = plugin->open(url, mutex, cond);
 		if (is != nullptr) {
 			is = input_rewind_open(is);
 
 			return InputStreamPtr(is);
-		} else if (error.IsDefined())
-			return nullptr;
+		}
 	}
 
-	error.Set(input_domain, "Unrecognized URI");
-	return nullptr;
+	throw std::runtime_error("Unrecognized URI");
 }
 
 InputStreamPtr
 InputStream::OpenReady(const char *uri,
-		       Mutex &mutex, Cond &cond,
-		       Error &error)
+		       Mutex &mutex, Cond &cond)
 {
-	auto is = Open(uri, mutex, cond, error);
-	if (is == nullptr)
-		return nullptr;
+	auto is = Open(uri, mutex, cond);
 
 	bool success;
 
 	{
 		const ScopeLock protect(mutex);
 		is->WaitReady();
+
+		Error error;
 		success = is->Check(error);
+		if (!success)
+			throw std::runtime_error(error.GetMessage());
 	}
 
 	if (!success)
diff --git a/src/input/plugins/AlsaInputPlugin.cxx b/src/input/plugins/AlsaInputPlugin.cxx
index 05098966a..70c6979d5 100644
--- a/src/input/plugins/AlsaInputPlugin.cxx
+++ b/src/input/plugins/AlsaInputPlugin.cxx
@@ -30,6 +30,7 @@
 #include "../AsyncInputStream.hxx"
 #include "util/Domain.hxx"
 #include "util/Error.hxx"
+#include "util/RuntimeError.hxx"
 #include "util/StringCompare.hxx"
 #include "util/ReusableArray.hxx"
 #include "util/ScopeExit.hxx"
@@ -102,8 +103,7 @@ public:
 		snd_pcm_close(capture_handle);
 	}
 
-	static InputStream *Create(const char *uri, Mutex &mutex, Cond &cond,
-				   Error &error);
+	static InputStream *Create(const char *uri, Mutex &mutex, Cond &cond);
 
 protected:
 	/* virtual methods from AsyncInputStream */
@@ -120,8 +120,7 @@ protected:
 
 private:
 	static snd_pcm_t *OpenDevice(const char *device, int rate,
-				     snd_pcm_format_t format, int channels,
-				     Error &error);
+				     snd_pcm_format_t format, int channels);
 
 	void Pause() {
 		AsyncInputStream::Pause();
@@ -143,8 +142,7 @@ private:
 };
 
 inline InputStream *
-AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond,
-			Error &error)
+AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond)
 {
 	const char *device = StringAfterPrefix(uri, "alsa://");
 	if (device == nullptr)
@@ -160,10 +158,7 @@ AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond,
 	snd_pcm_format_t format = default_format;
 	int channels = default_channels;
 
-	snd_pcm_t *handle = OpenDevice(device, rate, format, channels,
-				       error);
-	if (handle == nullptr)
-		return nullptr;
+	snd_pcm_t *handle = OpenDevice(device, rate, format, channels);
 
 	int frame_size = snd_pcm_format_width(format) / 8 * channels;
 	return new AlsaInputStream(io_thread_get(),
@@ -242,47 +237,40 @@ AlsaInputStream::Recover(int err)
 	return err;
 }
 
-static bool
+static void
 ConfigureCapture(snd_pcm_t *capture_handle,
-		 int rate, snd_pcm_format_t format, int channels,
-		 Error &error)
+		 int rate, snd_pcm_format_t format, int channels)
 {
 	int err;
 
 	snd_pcm_hw_params_t *hw_params;
-	if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
-		error.Format(alsa_input_domain, "Cannot allocate hardware parameter structure (%s)", snd_strerror(err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
+		throw FormatRuntimeError("Cannot allocate hardware parameter structure (%s)",
+					 snd_strerror(err));
 
 	AtScopeExit(hw_params) {
 		snd_pcm_hw_params_free(hw_params);
 	};
 
-	if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) {
-		error.Format(alsa_input_domain, "Cannot initialize hardware parameter structure (%s)", snd_strerror(err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0)
+		throw FormatRuntimeError("Cannot initialize hardware parameter structure (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set access type (%s)", snd_strerror (err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+		throw FormatRuntimeError("Cannot set access type (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, format)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set sample format (%s)", snd_strerror (err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, format)) < 0)
+		throw FormatRuntimeError("Cannot set sample format (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, channels)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set channels (%s)", snd_strerror (err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, channels)) < 0)
+		throw FormatRuntimeError("Cannot set channels (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, rate, 0)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set sample rate (%s)", snd_strerror (err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, rate, 0)) < 0)
+		throw FormatRuntimeError("Cannot set sample rate (%s)",
+					 snd_strerror(err));
 
 	/* period needs to be big enough so that poll() doesn't fire too often,
 	 * but small enough that buffer overruns don't occur if Read() is not
@@ -293,17 +281,13 @@ ConfigureCapture(snd_pcm_t *capture_handle,
 	snd_pcm_uframes_t period = read_buffer_size * 2;
 	int direction = -1;
 	if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params,
-							  &period, &direction)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set period size (%s)",
-			     snd_strerror(err));
-		return false;
-	}
+							  &period, &direction)) < 0)
+		throw FormatRuntimeError("Cannot set period size (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) {
-		error.Format(alsa_input_domain, "Cannot set parameters (%s)",
-			     snd_strerror(err));
-		return false;
-	}
+	if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
+		throw FormatRuntimeError("Cannot set parameters (%s)",
+					 snd_strerror(err));
 
 	snd_pcm_sw_params_t *sw_params;
 
@@ -315,37 +299,31 @@ ConfigureCapture(snd_pcm_t *capture_handle,
 	};
 
 	if ((err = snd_pcm_sw_params_set_start_threshold(capture_handle, sw_params,
-							 period)) < 0)  {
-		error.Format(alsa_input_domain,
-			     "unable to set start threshold (%s)", snd_strerror(err));
-		return false;
-	}
+							 period)) < 0)
+		throw FormatRuntimeError("unable to set start threshold (%s)",
+					 snd_strerror(err));
 
-	if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0) {
-		error.Format(alsa_input_domain,
-			     "unable to install sw params (%s)", snd_strerror(err));
-		return false;
-	}
-
-	return true;
+	if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0)
+		throw FormatRuntimeError("unable to install sw params (%s)",
+					 snd_strerror(err));
 }
 
 inline snd_pcm_t *
 AlsaInputStream::OpenDevice(const char *device,
-			    int rate, snd_pcm_format_t format, int channels,
-			    Error &error)
+			    int rate, snd_pcm_format_t format, int channels)
 {
 	snd_pcm_t *capture_handle;
 	int err;
 	if ((err = snd_pcm_open(&capture_handle, device,
-				SND_PCM_STREAM_CAPTURE, 0)) < 0) {
-		error.Format(alsa_input_domain, "Failed to open device: %s (%s)", device, snd_strerror(err));
-		return nullptr;
-	}
+				SND_PCM_STREAM_CAPTURE, 0)) < 0)
+		throw FormatRuntimeError("Failed to open device: %s (%s)",
+					 device, snd_strerror(err));
 
-	if (!ConfigureCapture(capture_handle, rate, format, channels, error)) {
+	try {
+		ConfigureCapture(capture_handle, rate, format, channels);
+	} catch (...) {
 		snd_pcm_close(capture_handle);
-		return nullptr;
+		throw;
 	}
 
 	snd_pcm_prepare(capture_handle);
@@ -356,9 +334,9 @@ AlsaInputStream::OpenDevice(const char *device,
 /*#########################  Plugin Functions  ##############################*/
 
 static InputStream *
-alsa_input_open(const char *uri, Mutex &mutex, Cond &cond, Error &error)
+alsa_input_open(const char *uri, Mutex &mutex, Cond &cond)
 {
-	return AlsaInputStream::Create(uri, mutex, cond, error);
+	return AlsaInputStream::Create(uri, mutex, cond);
 }
 
 const struct InputPlugin input_plugin_alsa = {
diff --git a/src/input/plugins/ArchiveInputPlugin.cxx b/src/input/plugins/ArchiveInputPlugin.cxx
index 590155d0e..3e307bfcc 100644
--- a/src/input/plugins/ArchiveInputPlugin.cxx
+++ b/src/input/plugins/ArchiveInputPlugin.cxx
@@ -25,14 +25,18 @@
 #include "archive/ArchivePlugin.hxx"
 #include "archive/ArchiveFile.hxx"
 #include "../InputPlugin.hxx"
+#include "../InputStream.hxx"
 #include "fs/Path.hxx"
 #include "Log.hxx"
 #include "util/ScopeExit.hxx"
+#include "util/Error.hxx"
+
+#include <stdexcept>
 
 #include <stdlib.h>
 
-InputStream *
-OpenArchiveInputStream(Path path, Mutex &mutex, Cond &cond, Error &error)
+InputStreamPtr
+OpenArchiveInputStream(Path path, Mutex &mutex, Cond &cond)
 {
 	const ArchivePlugin *arplug;
 
@@ -57,22 +61,21 @@ OpenArchiveInputStream(Path path, Mutex &mutex, Cond &cond, Error &error)
 		return nullptr;
 	}
 
+	Error error;
 	auto file = archive_file_open(arplug, Path::FromFS(archive), error);
-	if (file == nullptr) {
-		return nullptr;
-	}
+	if (file == nullptr)
+		throw std::runtime_error(error.GetMessage());
 
 	AtScopeExit(file) {
 		file->Close();
 	};
 
-	return file->OpenStream(filename, mutex, cond, error);
+	return InputStreamPtr(file->OpenStream(filename, mutex, cond));
 }
 
 static InputStream *
 input_archive_open(gcc_unused const char *filename,
-		gcc_unused Mutex &mutex, gcc_unused Cond &cond,
-		gcc_unused Error &error)
+		   gcc_unused Mutex &mutex, gcc_unused Cond &cond)
 {
 	/* dummy method; use OpenArchiveInputStream() instead */
 
diff --git a/src/input/plugins/ArchiveInputPlugin.hxx b/src/input/plugins/ArchiveInputPlugin.hxx
index 68d369206..b248dd277 100644
--- a/src/input/plugins/ArchiveInputPlugin.hxx
+++ b/src/input/plugins/ArchiveInputPlugin.hxx
@@ -20,15 +20,15 @@
 #ifndef MPD_INPUT_ARCHIVE_HXX
 #define MPD_INPUT_ARCHIVE_HXX
 
-class InputStream;
+#include "input/Ptr.hxx"
+
 class Path;
 class Mutex;
 class Cond;
-class Error;
 
 extern const struct InputPlugin input_plugin_archive;
 
-InputStream *
-OpenArchiveInputStream(Path path, Mutex &mutex, Cond &cond, Error &error);
+InputStreamPtr
+OpenArchiveInputStream(Path path, Mutex &mutex, Cond &cond);
 
 #endif
diff --git a/src/input/plugins/CdioParanoiaInputPlugin.cxx b/src/input/plugins/CdioParanoiaInputPlugin.cxx
index c9a3067f0..f06d01a10 100644
--- a/src/input/plugins/CdioParanoiaInputPlugin.cxx
+++ b/src/input/plugins/CdioParanoiaInputPlugin.cxx
@@ -127,7 +127,7 @@ struct cdio_uri {
 };
 
 static bool
-parse_cdio_uri(struct cdio_uri *dest, const char *src, Error &error)
+parse_cdio_uri(struct cdio_uri *dest, const char *src)
 {
 	if (!StringStartsWith(src, "cdda://"))
 		return false;
@@ -160,10 +160,8 @@ parse_cdio_uri(struct cdio_uri *dest, const char *src, Error &error)
 
 	char *endptr;
 	dest->track = strtoul(track, &endptr, 10);
-	if (*endptr != 0) {
-		error.Set(cdio_domain, "Malformed track number");
-		return false;
-	}
+	if (*endptr != 0)
+		throw std::runtime_error("Malformed track number");
 
 	if (endptr == track)
 		/* play the whole CD */
@@ -187,35 +185,28 @@ cdio_detect_device(void)
 
 static InputStream *
 input_cdio_open(const char *uri,
-		Mutex &mutex, Cond &cond,
-		Error &error)
+		Mutex &mutex, Cond &cond)
 {
 	struct cdio_uri parsed_uri;
-	if (!parse_cdio_uri(&parsed_uri, uri, error))
+	if (!parse_cdio_uri(&parsed_uri, uri))
 		return nullptr;
 
 	/* get list of CD's supporting CD-DA */
 	const AllocatedPath device = parsed_uri.device[0] != 0
 		? AllocatedPath::FromFS(parsed_uri.device)
 		: cdio_detect_device();
-	if (device.IsNull()) {
-		error.Set(cdio_domain,
-			  "Unable find or access a CD-ROM drive with an audio CD in it.");
-		return nullptr;
-	}
+	if (device.IsNull())
+		throw std::runtime_error("Unable find or access a CD-ROM drive with an audio CD in it.");
 
 	/* Found such a CD-ROM with a CD-DA loaded. Use the first drive in the list. */
 	const auto cdio = cdio_open(device.c_str(), DRIVER_UNKNOWN);
-	if (cdio == nullptr) {
-		error.Set(cdio_domain, "Failed to open CD drive");
-		return nullptr;
-	}
+	if (cdio == nullptr)
+		throw std::runtime_error("Failed to open CD drive");
 
 	const auto drv = cdio_cddap_identify_cdio(cdio, 1, nullptr);
 	if (drv == nullptr) {
-		error.Set(cdio_domain, "Unable to identify audio CD disc.");
 		cdio_destroy(cdio);
-		return nullptr;
+		throw std::runtime_error("Unable to identify audio CD disc.");
 	}
 
 	cdda_verbose_set(drv, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
@@ -223,12 +214,12 @@ input_cdio_open(const char *uri,
 	if (0 != cdio_cddap_open(drv)) {
 		cdio_cddap_close_no_free_cdio(drv);
 		cdio_destroy(cdio);
-		error.Set(cdio_domain, "Unable to open disc.");
-		return nullptr;
+		throw std::runtime_error("Unable to open disc.");
 	}
 
 	bool reverse_endian;
-	switch (data_bigendianp(drv)) {
+	const int be = data_bigendianp(drv);
+	switch (be) {
 	case -1:
 		LogDebug(cdio_domain, "drive returns unknown audio data");
 		reverse_endian = default_reverse_endian;
@@ -245,11 +236,10 @@ input_cdio_open(const char *uri,
 		break;
 
 	default:
-		error.Format(cdio_domain, "Drive returns unknown data type %d",
-			     data_bigendianp(drv));
 		cdio_cddap_close_no_free_cdio(drv);
 		cdio_destroy(cdio);
-		return nullptr;
+		throw FormatRuntimeError("Drive returns unknown data type %d",
+					 be);
 	}
 
 	lsn_t lsn_from, lsn_to;
diff --git a/src/input/plugins/CurlInputPlugin.cxx b/src/input/plugins/CurlInputPlugin.cxx
index 38375d2f6..2d1df3a26 100644
--- a/src/input/plugins/CurlInputPlugin.cxx
+++ b/src/input/plugins/CurlInputPlugin.cxx
@@ -32,6 +32,7 @@
 #include "util/ASCII.hxx"
 #include "util/StringUtil.hxx"
 #include "util/NumberParser.hxx"
+#include "util/RuntimeError.hxx"
 #include "util/Error.hxx"
 #include "util/Domain.hxx"
 #include "Log.hxx"
@@ -78,17 +79,18 @@ struct CurlInputStream final : public AsyncInputStream {
 				  CURL_MAX_BUFFERED,
 				  CURL_RESUME_AT),
 		 request_headers(nullptr),
-		 icy(new IcyInputStream(this)) {}
+		 icy(new IcyInputStream(this)) {
+		InitEasy();
+	}
 
 	~CurlInputStream();
 
 	CurlInputStream(const CurlInputStream &) = delete;
 	CurlInputStream &operator=(const CurlInputStream &) = delete;
 
-	static InputStream *Open(const char *url, Mutex &mutex, Cond &cond,
-				 Error &error);
+	static InputStream *Open(const char *url, Mutex &mutex, Cond &cond);
 
-	bool InitEasy(Error &error);
+	void InitEasy();
 
 	/**
 	 * Frees the current "libcurl easy" handle, and everything
@@ -201,7 +203,7 @@ public:
 		curl_multi_cleanup(multi);
 	}
 
-	bool Add(CurlInputStream *c, Error &error);
+	void Add(CurlInputStream *c);
 	void Remove(CurlInputStream *c);
 
 	/**
@@ -355,41 +357,39 @@ CurlSocket::OnSocketReady(unsigned flags)
 
 /**
  * Runs in the I/O thread.  No lock needed.
+ *
+ * Throws std::runtime_error on error.
  */
-inline bool
-CurlMulti::Add(CurlInputStream *c, Error &error)
+inline void
+CurlMulti::Add(CurlInputStream *c)
 {
 	assert(io_thread_inside());
 	assert(c != nullptr);
 	assert(c->easy != nullptr);
 
 	CURLMcode mcode = curl_multi_add_handle(multi, c->easy);
-	if (mcode != CURLM_OK) {
-		error.Format(curlm_domain, mcode,
-			     "curl_multi_add_handle() failed: %s",
-			     curl_multi_strerror(mcode));
-		return false;
-	}
+	if (mcode != CURLM_OK)
+		throw FormatRuntimeError("curl_multi_add_handle() failed: %s",
+					 curl_multi_strerror(mcode));
 
 	InvalidateSockets();
-	return true;
 }
 
 /**
  * Call input_curl_easy_add() in the I/O thread.  May be called from
  * any thread.  Caller must not hold a mutex.
+ *
+ * Throws std::runtime_error on error.
  */
-static bool
-input_curl_easy_add_indirect(CurlInputStream *c, Error &error)
+static void
+input_curl_easy_add_indirect(CurlInputStream *c)
 {
 	assert(c != nullptr);
 	assert(c->easy != nullptr);
 
-	bool result;
-	BlockingCall(io_thread_get(), [c, &error, &result](){
-			result = curl_multi->Add(c, error);
+	BlockingCall(io_thread_get(), [c](){
+			curl_multi->Add(c);
 		});
-	return result;
 }
 
 inline void
@@ -727,14 +727,12 @@ input_curl_writefunction(void *ptr, size_t size, size_t nmemb, void *stream)
 	return c.DataReceived(ptr, size);
 }
 
-bool
-CurlInputStream::InitEasy(Error &error)
+void
+CurlInputStream::InitEasy()
 {
 	easy = curl_easy_init();
-	if (easy == nullptr) {
-		error.Set(curl_domain, "curl_easy_init() failed");
-		return false;
-	}
+	if (easy == nullptr)
+		throw std::runtime_error("curl_easy_init() failed");
 
 	curl_easy_setopt(easy, CURLOPT_PRIVATE, (void *)this);
 	curl_easy_setopt(easy, CURLOPT_USERAGENT,
@@ -773,19 +771,14 @@ CurlInputStream::InitEasy(Error &error)
 	curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, verify_host ? 2l : 0l);
 
 	CURLcode code = curl_easy_setopt(easy, CURLOPT_URL, GetURI());
-	if (code != CURLE_OK) {
-		error.Format(curl_domain, code,
-			     "curl_easy_setopt() failed: %s",
-			     curl_easy_strerror(code));
-		return false;
-	}
+	if (code != CURLE_OK)
+		throw FormatRuntimeError("curl_easy_setopt() failed: %s",
+					 curl_easy_strerror(code));
 
 	request_headers = nullptr;
 	request_headers = curl_slist_append(request_headers,
 					       "Icy-Metadata: 1");
 	curl_easy_setopt(easy, CURLOPT_HTTPHEADER, request_headers);
-
-	return true;
 }
 
 void
@@ -809,11 +802,11 @@ CurlInputStream::DoSeek(offset_type new_offset)
 		return;
 	}
 
-	Error error;
-	if (!InitEasy(postponed_error)) {
+	try {
+		InitEasy();
+	} catch (...) {
 		mutex.lock();
-		PostponeError(std::move(error));
-		return;
+		throw;
 	}
 
 	/* send the "Range" header */
@@ -823,10 +816,11 @@ CurlInputStream::DoSeek(offset_type new_offset)
 		curl_easy_setopt(easy, CURLOPT_RANGE, range);
 	}
 
-	if (!input_curl_easy_add_indirect(this, error)) {
+	try {
+		input_curl_easy_add_indirect(this);
+	} catch (...) {
 		mutex.lock();
-		PostponeError(std::move(error));
-		return;
+		throw;
 	}
 
 	mutex.lock();
@@ -834,27 +828,29 @@ CurlInputStream::DoSeek(offset_type new_offset)
 }
 
 inline InputStream *
-CurlInputStream::Open(const char *url, Mutex &mutex, Cond &cond,
-		      Error &error)
+CurlInputStream::Open(const char *url, Mutex &mutex, Cond &cond)
 {
 	CurlInputStream *c = new CurlInputStream(url, mutex, cond);
-	if (!c->InitEasy(error) || !input_curl_easy_add_indirect(c, error)) {
+
+	try {
+		c->InitEasy();
+		input_curl_easy_add_indirect(c);
+	} catch (...) {
 		delete c;
-		return nullptr;
+		throw;
 	}
 
 	return c->icy;
 }
 
 static InputStream *
-input_curl_open(const char *url, Mutex &mutex, Cond &cond,
-		Error &error)
+input_curl_open(const char *url, Mutex &mutex, Cond &cond)
 {
 	if (memcmp(url, "http://",  7) != 0 &&
 	    memcmp(url, "https://", 8) != 0)
 		return nullptr;
 
-	return CurlInputStream::Open(url, mutex, cond, error);
+	return CurlInputStream::Open(url, mutex, cond);
 }
 
 const struct InputPlugin input_plugin_curl = {
diff --git a/src/input/plugins/FfmpegInputPlugin.cxx b/src/input/plugins/FfmpegInputPlugin.cxx
index 1eb91f421..ab189c490 100644
--- a/src/input/plugins/FfmpegInputPlugin.cxx
+++ b/src/input/plugins/FfmpegInputPlugin.cxx
@@ -84,8 +84,7 @@ input_ffmpeg_init(gcc_unused const ConfigBlock &block)
 
 static InputStream *
 input_ffmpeg_open(const char *uri,
-		  Mutex &mutex, Cond &cond,
-		  Error &error)
+		  Mutex &mutex, Cond &cond)
 {
 	if (!StringStartsWith(uri, "gopher://") &&
 	    !StringStartsWith(uri, "rtp://") &&
@@ -97,10 +96,8 @@ input_ffmpeg_open(const char *uri,
 
 	AVIOContext *h;
 	auto result = avio_open(&h, uri, AVIO_FLAG_READ);
-	if (result != 0) {
-		SetFfmpegError(error, result);
-		return nullptr;
-	}
+	if (result != 0)
+		throw MakeFfmpegError(result);
 
 	return new FfmpegInputStream(uri, mutex, cond, h);
 }
diff --git a/src/input/plugins/FileInputPlugin.cxx b/src/input/plugins/FileInputPlugin.cxx
index 1d5eaf90e..6fb816423 100644
--- a/src/input/plugins/FileInputPlugin.cxx
+++ b/src/input/plugins/FileInputPlugin.cxx
@@ -21,18 +21,15 @@
 #include "FileInputPlugin.hxx"
 #include "../InputStream.hxx"
 #include "../InputPlugin.hxx"
-#include "util/Error.hxx"
-#include "util/Domain.hxx"
 #include "fs/Path.hxx"
 #include "fs/FileInfo.hxx"
 #include "fs/io/FileReader.hxx"
 #include "system/FileDescriptor.hxx"
+#include "util/RuntimeError.hxx"
 
 #include <sys/stat.h>
 #include <fcntl.h>
 
-static constexpr Domain file_domain("file");
-
 class FileInputStream final : public InputStream {
 	FileReader reader;
 
@@ -56,38 +53,31 @@ public:
 	bool Seek(offset_type offset, Error &error) override;
 };
 
-InputStream *
+InputStreamPtr
 OpenFileInputStream(Path path,
-		    Mutex &mutex, Cond &cond,
-		    Error &error)
-try {
+		    Mutex &mutex, Cond &cond)
+{
 	FileReader reader(path);
 
 	const FileInfo info = reader.GetFileInfo();
 
-	if (!info.IsRegular()) {
-		error.Format(file_domain, "Not a regular file: %s",
-			     path.c_str());
-		return nullptr;
-	}
+	if (!info.IsRegular())
+		throw FormatRuntimeError("Not a regular file: %s",
+					 path.c_str());
 
 #ifdef POSIX_FADV_SEQUENTIAL
 	posix_fadvise(reader.GetFD().Get(), (off_t)0, info.GetSize(),
 		      POSIX_FADV_SEQUENTIAL);
 #endif
 
-	return new FileInputStream(path.ToUTF8().c_str(),
-				   std::move(reader), info.GetSize(),
-				   mutex, cond);
-} catch (const std::exception &e) {
-	error.Set(std::current_exception());
-	return nullptr;
+	return InputStreamPtr(new FileInputStream(path.ToUTF8().c_str(),
+						  std::move(reader), info.GetSize(),
+						  mutex, cond));
 }
 
 static InputStream *
 input_file_open(gcc_unused const char *filename,
-		gcc_unused Mutex &mutex, gcc_unused Cond &cond,
-		gcc_unused Error &error)
+		gcc_unused Mutex &mutex, gcc_unused Cond &cond)
 {
 	/* dummy method; use OpenFileInputStream() instead */
 
diff --git a/src/input/plugins/FileInputPlugin.hxx b/src/input/plugins/FileInputPlugin.hxx
index b508c9900..c967d84d6 100644
--- a/src/input/plugins/FileInputPlugin.hxx
+++ b/src/input/plugins/FileInputPlugin.hxx
@@ -20,17 +20,16 @@
 #ifndef MPD_INPUT_FILE_HXX
 #define MPD_INPUT_FILE_HXX
 
-class InputStream;
+#include "input/Ptr.hxx"
+
 class Path;
 class Mutex;
 class Cond;
-class Error;
 
 extern const struct InputPlugin input_plugin_file;
 
-InputStream *
+InputStreamPtr
 OpenFileInputStream(Path path,
-		    Mutex &mutex, Cond &cond,
-		    Error &error);
+		    Mutex &mutex, Cond &cond);
 
 #endif
diff --git a/src/input/plugins/MmsInputPlugin.cxx b/src/input/plugins/MmsInputPlugin.cxx
index 25af267e1..9649cc85f 100644
--- a/src/input/plugins/MmsInputPlugin.cxx
+++ b/src/input/plugins/MmsInputPlugin.cxx
@@ -72,8 +72,7 @@ MmsInputStream::Open(Error &error)
 
 static InputStream *
 input_mms_open(const char *url,
-	       Mutex &mutex, Cond &cond,
-	       gcc_unused Error &error)
+	       Mutex &mutex, Cond &cond)
 {
 	if (!StringStartsWith(url, "mms://") &&
 	    !StringStartsWith(url, "mmsh://") &&
diff --git a/src/input/plugins/NfsInputPlugin.cxx b/src/input/plugins/NfsInputPlugin.cxx
index b2a9c8c30..ddacf317f 100644
--- a/src/input/plugins/NfsInputPlugin.cxx
+++ b/src/input/plugins/NfsInputPlugin.cxx
@@ -57,10 +57,10 @@ public:
 		DeferClose();
 	}
 
-	bool Open(Error &error) {
+	void Open() {
 		assert(!IsReady());
 
-		return NfsFileReader::Open(GetURI(), error);
+		NfsFileReader::Open(GetURI());
 	}
 
 private:
@@ -119,17 +119,10 @@ NfsInputStream::DoResume()
 		reconnect_on_resume = false;
 		reconnecting = true;
 
-		mutex.unlock();
+		ScopeUnlock unlock(mutex);
+
 		NfsFileReader::Close();
-
-		Error error;
-		bool success = NfsFileReader::Open(GetURI(), error);
-		mutex.lock();
-
-		if (!success) {
-			postponed_error = std::move(error);
-			cond.broadcast();
-		}
+		NfsFileReader::Open(GetURI());
 
 		return;
 	}
@@ -229,16 +222,17 @@ input_nfs_finish()
 
 static InputStream *
 input_nfs_open(const char *uri,
-	       Mutex &mutex, Cond &cond,
-	       Error &error)
+	       Mutex &mutex, Cond &cond)
 {
 	if (!StringStartsWith(uri, "nfs://"))
 		return nullptr;
 
 	NfsInputStream *is = new NfsInputStream(uri, mutex, cond);
-	if (!is->Open(error)) {
+	try {
+		is->Open();
+	} catch (...) {
 		delete is;
-		return nullptr;
+		throw;
 	}
 
 	return is;
diff --git a/src/input/plugins/SmbclientInputPlugin.cxx b/src/input/plugins/SmbclientInputPlugin.cxx
index d74afa828..7eea671f0 100644
--- a/src/input/plugins/SmbclientInputPlugin.cxx
+++ b/src/input/plugins/SmbclientInputPlugin.cxx
@@ -24,6 +24,7 @@
 #include "../InputStream.hxx"
 #include "../InputPlugin.hxx"
 #include "PluginUnavailable.hxx"
+#include "system/Error.hxx"
 #include "util/StringCompare.hxx"
 #include "util/Error.hxx"
 
@@ -85,8 +86,7 @@ input_smbclient_init(gcc_unused const ConfigBlock &block)
 
 static InputStream *
 input_smbclient_open(const char *uri,
-		     Mutex &mutex, Cond &cond,
-		     Error &error)
+		     Mutex &mutex, Cond &cond)
 {
 	if (!StringStartsWith(uri, "smb://"))
 		return nullptr;
@@ -94,33 +94,30 @@ input_smbclient_open(const char *uri,
 	const ScopeLock protect(smbclient_mutex);
 
 	SMBCCTX *ctx = smbc_new_context();
-	if (ctx == nullptr) {
-		error.SetErrno("smbc_new_context() failed");
-		return nullptr;
-	}
+	if (ctx == nullptr)
+		throw MakeErrno("smbc_new_context() failed");
 
 	SMBCCTX *ctx2 = smbc_init_context(ctx);
 	if (ctx2 == nullptr) {
-		error.SetErrno("smbc_init_context() failed");
+		int e = errno;
 		smbc_free_context(ctx, 1);
-		return nullptr;
+		throw MakeErrno(e, "smbc_init_context() failed");
 	}
 
 	ctx = ctx2;
 
 	int fd = smbc_open(uri, O_RDONLY, 0);
 	if (fd < 0) {
-		error.SetErrno("smbc_open() failed");
+		int e = errno;
 		smbc_free_context(ctx, 1);
-		return nullptr;
+		throw MakeErrno(e, "smbc_open() failed");
 	}
 
 	struct stat st;
 	if (smbc_fstat(fd, &st) < 0) {
-		error.SetErrno("smbc_fstat() failed");
-		smbc_close(fd);
+		int e = errno;
 		smbc_free_context(ctx, 1);
-		return nullptr;
+		throw MakeErrno(e, "smbc_fstat() failed");
 	}
 
 	return new SmbclientInputStream(uri, mutex, cond, ctx, fd, st);
diff --git a/src/lib/ffmpeg/Error.cxx b/src/lib/ffmpeg/Error.cxx
index 9dc193bfa..92608300f 100644
--- a/src/lib/ffmpeg/Error.cxx
+++ b/src/lib/ffmpeg/Error.cxx
@@ -26,6 +26,14 @@ extern "C" {
 #include <libavutil/error.h>
 }
 
+std::runtime_error
+MakeFfmpegError(int errnum)
+{
+	char msg[256];
+	av_strerror(errnum, msg, sizeof(msg));
+	return std::runtime_error(msg);
+}
+
 void
 SetFfmpegError(Error &error, int errnum)
 {
diff --git a/src/lib/ffmpeg/Error.hxx b/src/lib/ffmpeg/Error.hxx
index 7dc7fc550..3a6eb876d 100644
--- a/src/lib/ffmpeg/Error.hxx
+++ b/src/lib/ffmpeg/Error.hxx
@@ -20,8 +20,13 @@
 #ifndef MPD_FFMPEG_ERROR_HXX
 #define MPD_FFMPEG_ERROR_HXX
 
+#include <stdexcept>
+
 class Error;
 
+std::runtime_error
+MakeFfmpegError(int errnum);
+
 void
 SetFfmpegError(Error &error, int errnum);
 
diff --git a/src/lib/nfs/FileReader.cxx b/src/lib/nfs/FileReader.cxx
index 523690de7..54b59e09a 100644
--- a/src/lib/nfs/FileReader.cxx
+++ b/src/lib/nfs/FileReader.cxx
@@ -91,23 +91,19 @@ NfsFileReader::DeferClose()
 	BlockingCall(io_thread_get(), [this](){ Close(); });
 }
 
-bool
-NfsFileReader::Open(const char *uri, Error &error)
+void
+NfsFileReader::Open(const char *uri)
 {
 	assert(state == State::INITIAL);
 
-	if (!StringStartsWith(uri, "nfs://")) {
-		error.Set(nfs_domain, "Malformed nfs:// URI");
-		return false;
-	}
+	if (!StringStartsWith(uri, "nfs://"))
+		throw std::runtime_error("Malformed nfs:// URI");
 
 	uri += 6;
 
 	const char *slash = strchr(uri, '/');
-	if (slash == nullptr) {
-		error.Set(nfs_domain, "Malformed nfs:// URI");
-		return false;
-	}
+	if (slash == nullptr)
+		throw std::runtime_error("Malformed nfs:// URI");
 
 	server = std::string(uri, slash);
 
@@ -121,10 +117,8 @@ NfsFileReader::Open(const char *uri, Error &error)
 		path = new_path;
 	} else {
 		slash = strrchr(uri + 1, '/');
-		if (slash == nullptr || slash[1] == 0) {
-			error.Set(nfs_domain, "Malformed nfs:// URI");
-			return false;
-		}
+		if (slash == nullptr || slash[1] == 0)
+			throw std::runtime_error("Malformed nfs:// URI");
 
 		export_name = std::string(uri, slash);
 		path = slash;
@@ -132,7 +126,6 @@ NfsFileReader::Open(const char *uri, Error &error)
 
 	state = State::DEFER;
 	DeferredMonitor::Schedule();
-	return true;
 }
 
 bool
diff --git a/src/lib/nfs/FileReader.hxx b/src/lib/nfs/FileReader.hxx
index ab88299cc..177fb66f2 100644
--- a/src/lib/nfs/FileReader.hxx
+++ b/src/lib/nfs/FileReader.hxx
@@ -61,7 +61,11 @@ public:
 	void Close();
 	void DeferClose();
 
-	bool Open(const char *uri, Error &error);
+	/**
+	 * Throws std::runtime_error on error.
+	 */
+	void Open(const char *uri);
+
 	bool Read(uint64_t offset, size_t size, Error &error);
 	void CancelRead();
 
diff --git a/src/playlist/PlaylistStream.cxx b/src/playlist/PlaylistStream.cxx
index 37d7663de..fc3a5586b 100644
--- a/src/playlist/PlaylistStream.cxx
+++ b/src/playlist/PlaylistStream.cxx
@@ -44,13 +44,7 @@ try {
 	if (!playlist_suffix_supported(suffix_utf8.c_str()))
 		return nullptr;
 
-	Error error;
-	auto is = OpenLocalInputStream(path, mutex, cond, error);
-	if (is == nullptr) {
-		LogError(error);
-		return nullptr;
-	}
-
+	auto is = OpenLocalInputStream(path, mutex, cond);
 	return playlist_list_open_stream_suffix(std::move(is),
 						suffix_utf8.c_str());
 } catch (const std::runtime_error &e) {
@@ -85,15 +79,7 @@ try {
 	if (playlist != nullptr)
 		return playlist;
 
-	Error error;
-	auto is = InputStream::OpenReady(uri, mutex, cond, error);
-	if (is == nullptr) {
-		if (error.IsDefined())
-			FormatError(error, "Failed to open %s", uri);
-
-		return nullptr;
-	}
-
+	auto is = InputStream::OpenReady(uri, mutex, cond);
 	return playlist_list_open_stream(std::move(is), uri);
 } catch (const std::runtime_error &e) {
 	LogError(e);
diff --git a/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
index e18d1abab..67a64ce2e 100644
--- a/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
+++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
@@ -230,15 +230,9 @@ static constexpr yajl_callbacks parse_callbacks = {
 static int
 soundcloud_parse_json(const char *url, yajl_handle hand,
 		      Mutex &mutex, Cond &cond)
-{
+try {
 	Error error;
-	auto input_stream = InputStream::OpenReady(url, mutex, cond,
-						   error);
-	if (input_stream == nullptr) {
-		if (error.IsDefined())
-			LogError(error);
-		return -1;
-	}
+	auto input_stream = InputStream::OpenReady(url, mutex, cond);
 
 	const ScopeLock protect(mutex);
 
@@ -275,6 +269,9 @@ soundcloud_parse_json(const char *url, yajl_handle hand,
 	}
 
 	return 0;
+} catch (const std::exception &e) {
+	LogError(e);
+	return -1;
 }
 
 /**
diff --git a/src/system/Error.hxx b/src/system/Error.hxx
index 4c9ad8613..7adf7921e 100644
--- a/src/system/Error.hxx
+++ b/src/system/Error.hxx
@@ -140,6 +140,19 @@ IsFileNotFound(const std::system_error &e)
 #endif
 }
 
+gcc_pure
+static inline bool
+IsPathNotFound(const std::system_error &e)
+{
+#ifdef WIN32
+	return e.code().category() == std::system_category() &&
+		e.code().value() == ERROR_PATH_NOT_FOUND;
+#else
+	return e.code().category() == std::system_category() &&
+		e.code().value() == ENOTDIR;
+#endif
+}
+
 gcc_pure
 static inline bool
 IsAccessDenied(const std::system_error &e)
diff --git a/src/tag/Generic.cxx b/src/tag/Generic.cxx
index 4c2eee63c..ad2f1e6e1 100644
--- a/src/tag/Generic.cxx
+++ b/src/tag/Generic.cxx
@@ -53,13 +53,7 @@ try {
 	Mutex mutex;
 	Cond cond;
 
-	Error error;
-	auto is = OpenLocalInputStream(path, mutex, cond, error);
-	if (!is) {
-		LogError(error);
-		return false;
-	}
-
+	auto is = OpenLocalInputStream(path, mutex, cond);
 	return ScanGenericTags(*is, handler, ctx);
 } catch (const std::runtime_error &e) {
 	LogError(e);
diff --git a/test/FakeDecoderAPI.cxx b/test/FakeDecoderAPI.cxx
index e4f5d132f..d2dffcf18 100644
--- a/test/FakeDecoderAPI.cxx
+++ b/test/FakeDecoderAPI.cxx
@@ -74,9 +74,9 @@ decoder_seek_error(gcc_unused Decoder &decoder)
 }
 
 InputStreamPtr
-decoder_open_uri(Decoder &decoder, const char *uri, Error &error)
+decoder_open_uri(Decoder &decoder, const char *uri)
 {
-	return InputStream::OpenReady(uri, decoder.mutex, decoder.cond, error);
+	return InputStream::OpenReady(uri, decoder.mutex, decoder.cond);
 }
 
 size_t
diff --git a/test/ReadApeTags.cxx b/test/ReadApeTags.cxx
index 7c81520f4..2b74dba77 100644
--- a/test/ReadApeTags.cxx
+++ b/test/ReadApeTags.cxx
@@ -26,7 +26,6 @@
 #include "input/InputStream.hxx"
 #include "input/LocalOpen.hxx"
 #include "util/StringView.hxx"
-#include "util/Error.hxx"
 
 #include <stdlib.h>
 #include <stdio.h>
@@ -49,7 +48,7 @@ MyApeTagCallback(gcc_unused unsigned long flags,
 
 int
 main(int argc, char **argv)
-{
+try {
 #ifdef HAVE_LOCALE_H
 	/* initialize locale */
 	setlocale(LC_CTYPE,"");
@@ -65,12 +64,7 @@ main(int argc, char **argv)
 	Mutex mutex;
 	Cond cond;
 
-	Error error;
-	auto is = OpenLocalInputStream(path, mutex, cond, error);
-	if (!is) {
-		LogError(error);
-		return EXIT_FAILURE;
-	}
+	auto is = OpenLocalInputStream(path, mutex, cond);
 
 	if (!tag_ape_scan(*is, MyApeTagCallback)) {
 		fprintf(stderr, "error\n");
@@ -78,4 +72,7 @@ main(int argc, char **argv)
 	}
 
 	return EXIT_SUCCESS;
+} catch (const std::exception &e) {
+	LogError(e);
+	return EXIT_FAILURE;
 }
diff --git a/test/dump_playlist.cxx b/test/dump_playlist.cxx
index 1bb03b598..8f3be7f7a 100644
--- a/test/dump_playlist.cxx
+++ b/test/dump_playlist.cxx
@@ -31,7 +31,6 @@
 #include "fs/Path.hxx"
 #include "fs/io/BufferedOutputStream.hxx"
 #include "fs/io/StdioOutputStream.hxx"
-#include "util/Error.hxx"
 #include "thread/Cond.hxx"
 #include "Log.hxx"
 
@@ -63,7 +62,6 @@ try {
 
 	config_global_init();
 
-	Error error;
 	ReadConfigFile(config_path);
 
 	const ScopeIOThread io_thread;
@@ -82,15 +80,7 @@ try {
 	if (playlist == NULL) {
 		/* open the stream and wait until it becomes ready */
 
-		is = InputStream::OpenReady(uri, mutex, cond, error);
-		if (!is) {
-			if (error.IsDefined())
-				LogError(error);
-			else
-				fprintf(stderr,
-					"InputStream::Open() failed\n");
-			return 2;
-		}
+		is = InputStream::OpenReady(uri, mutex, cond);
 
 		/* open the playlist */
 
diff --git a/test/dump_rva2.cxx b/test/dump_rva2.cxx
index 4b716b843..c4408b1b5 100644
--- a/test/dump_rva2.cxx
+++ b/test/dump_rva2.cxx
@@ -24,7 +24,6 @@
 #include "config/ConfigGlobal.hxx"
 #include "thread/Mutex.hxx"
 #include "thread/Cond.hxx"
-#include "util/Error.hxx"
 #include "fs/Path.hxx"
 #include "input/InputStream.hxx"
 #include "input/LocalOpen.hxx"
@@ -47,7 +46,7 @@ config_get_string(gcc_unused enum ConfigOption option,
 }
 
 int main(int argc, char **argv)
-{
+try {
 #ifdef HAVE_LOCALE_H
 	/* initialize locale */
 	setlocale(LC_CTYPE,"");
@@ -63,12 +62,7 @@ int main(int argc, char **argv)
 	Mutex mutex;
 	Cond cond;
 
-	Error error;
-	auto is = OpenLocalInputStream(path, mutex, cond, error);
-	if (!is) {
-		LogError(error);
-		return EXIT_FAILURE;
-	}
+	auto is = OpenLocalInputStream(path, mutex, cond);
 
 	const auto tag = tag_id3_load(*is);
 	if (tag == NULL) {
@@ -96,4 +90,7 @@ int main(int argc, char **argv)
 			tuple->gain, tuple->peak);
 
 	return EXIT_SUCCESS;
+} catch (const std::exception &e) {
+	LogError(e);
+	return EXIT_FAILURE;
 }
diff --git a/test/dump_text_file.cxx b/test/dump_text_file.cxx
index 7934349d6..512a13102 100644
--- a/test/dump_text_file.cxx
+++ b/test/dump_text_file.cxx
@@ -83,7 +83,6 @@ try {
 	archive_plugin_init_all();
 #endif
 
-	Error error;
 	input_stream_global_init();
 
 	/* open the stream and dump it */
@@ -92,16 +91,8 @@ try {
 		Mutex mutex;
 		Cond cond;
 
-		auto is = InputStream::OpenReady(argv[1], mutex, cond, error);
-		if (is) {
-			ret = dump_input_stream(std::move(is));
-		} else {
-			if (error.IsDefined())
-				LogError(error);
-			else
-				fprintf(stderr, "input_stream::Open() failed\n");
-			ret = EXIT_FAILURE;
-		}
+		auto is = InputStream::OpenReady(argv[1], mutex, cond);
+		ret = dump_input_stream(std::move(is));
 	}
 
 	/* deinitialize everything */
diff --git a/test/read_tags.cxx b/test/read_tags.cxx
index 17833516e..a345f87bf 100644
--- a/test/read_tags.cxx
+++ b/test/read_tags.cxx
@@ -26,7 +26,6 @@
 #include "AudioFormat.hxx"
 #include "tag/TagHandler.hxx"
 #include "tag/Generic.hxx"
-#include "util/Error.hxx"
 #include "fs/Path.hxx"
 #include "thread/Cond.hxx"
 #include "Log.hxx"
@@ -89,7 +88,6 @@ try {
 
 	const ScopeIOThread io_thread;
 
-	Error error;
 	input_stream_global_init();
 	decoder_plugin_init_all();
 
@@ -105,13 +103,7 @@ try {
 		Cond cond;
 
 		auto is = InputStream::OpenReady(path.c_str(),
-						 mutex, cond,
-						 error);
-		if (!is) {
-			FormatError(error, "Failed to open %s", path.c_str());
-			return EXIT_FAILURE;
-		}
-
+						 mutex, cond);
 		success = plugin->ScanStream(*is, print_handler, nullptr);
 	}
 
diff --git a/test/run_decoder.cxx b/test/run_decoder.cxx
index 19abf0e7e..f8f139825 100644
--- a/test/run_decoder.cxx
+++ b/test/run_decoder.cxx
@@ -26,7 +26,6 @@
 #include "input/InputStream.hxx"
 #include "fs/Path.hxx"
 #include "AudioFormat.hxx"
-#include "util/Error.hxx"
 #include "Log.hxx"
 
 #include <stdexcept>
@@ -49,7 +48,6 @@ try {
 
 	const ScopeIOThread io_thread;
 
-	Error error;
 	input_stream_global_init();
 
 	decoder_plugin_init_all();
@@ -64,16 +62,7 @@ try {
 		plugin->FileDecode(decoder, Path::FromFS(uri));
 	} else if (plugin->stream_decode != nullptr) {
 		auto is = InputStream::OpenReady(uri, decoder.mutex,
-						 decoder.cond, error);
-		if (!is) {
-			if (error.IsDefined())
-				LogError(error);
-			else
-				fprintf(stderr, "InputStream::Open() failed\n");
-
-			return EXIT_FAILURE;
-		}
-
+						 decoder.cond);
 		plugin->StreamDecode(decoder, *is);
 	} else {
 		fprintf(stderr, "Decoder plugin is not usable\n");
diff --git a/test/run_input.cxx b/test/run_input.cxx
index 726758506..675f0ced3 100644
--- a/test/run_input.cxx
+++ b/test/run_input.cxx
@@ -118,16 +118,8 @@ try {
 	{
 		Mutex mutex;
 		Cond cond;
-		auto is = InputStream::OpenReady(argv[1], mutex, cond, error);
-		if (is) {
-			ret = dump_input_stream(is.get());
-		} else {
-			if (error.IsDefined())
-				LogError(error);
-			else
-				fprintf(stderr, "input_stream::Open() failed\n");
-			ret = EXIT_FAILURE;
-		}
+		auto is = InputStream::OpenReady(argv[1], mutex, cond);
+		ret = dump_input_stream(is.get());
 	}
 
 	/* deinitialize everything */