diff --git a/src/db/meson.build b/src/db/meson.build
index 694bd5204..bef22224f 100644
--- a/src/db/meson.build
+++ b/src/db/meson.build
@@ -43,7 +43,6 @@ db_glue_sources = [
 if enable_inotify
   db_glue_sources += [
     'update/InotifyDomain.cxx',
-    'update/InotifySource.cxx',
     'update/InotifyQueue.cxx',
     'update/InotifyUpdate.cxx',
   ]
diff --git a/src/db/update/InotifySource.cxx b/src/db/update/InotifySource.cxx
deleted file mode 100644
index 827998e6b..000000000
--- a/src/db/update/InotifySource.cxx
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "InotifySource.hxx"
-#include "InotifyDomain.hxx"
-#include "io/FileDescriptor.hxx"
-#include "system/Error.hxx"
-#include "Log.hxx"
-
-#include <cerrno>
-#include <climits>
-#include <cstdint>
-
-#include <sys/inotify.h>
-
-void
-InotifySource::OnSocketReady([[maybe_unused]] unsigned flags) noexcept
-{
-	uint8_t buffer[4096];
-	static_assert(sizeof(buffer) >= sizeof(struct inotify_event) + NAME_MAX + 1,
-		      "inotify buffer too small");
-
-	auto ifd = socket_event.GetFileDescriptor();
-	ssize_t nbytes = ifd.Read(buffer, sizeof(buffer));
-	if (nbytes <= 0) {
-		if (nbytes < 0)
-			FmtError(inotify_domain,
-				 "Failed to read from inotify: {}",
-				 strerror(errno));
-		else
-			LogError(inotify_domain,
-				 "end of file from inotify");
-		socket_event.Cancel();
-		return;
-	}
-
-	const uint8_t *p = buffer, *const end = p + nbytes;
-
-	while (true) {
-		const size_t remaining = end - p;
-		const auto *event =
-			(const struct inotify_event *)p;
-		if (remaining < sizeof(*event) ||
-		    remaining < sizeof(*event) + event->len)
-			break;
-
-		const char *name;
-		if (event->len > 0 && event->name[event->len - 1] == 0)
-			name = event->name;
-		else
-			name = nullptr;
-
-		callback(event->wd, event->mask, name, callback_ctx);
-		p += sizeof(*event) + event->len;
-	}
-}
-
-static FileDescriptor
-InotifyInit()
-{
-	FileDescriptor fd;
-	if (!fd.CreateInotify())
-		throw MakeErrno("inotify_init() has failed");
-
-	return fd;
-}
-
-InotifySource::InotifySource(EventLoop &_loop,
-			     mpd_inotify_callback_t _callback, void *_ctx)
-	:socket_event(_loop, BIND_THIS_METHOD(OnSocketReady),
-		      InotifyInit()),
-	 callback(_callback), callback_ctx(_ctx)
-{
-	socket_event.ScheduleRead();
-}
-
-int
-InotifySource::Add(const char *path_fs, unsigned mask)
-{
-	auto ifd = socket_event.GetFileDescriptor();
-	int wd = inotify_add_watch(ifd.Get(), path_fs, mask);
-	if (wd < 0)
-		throw MakeErrno("inotify_add_watch() has failed");
-
-	return wd;
-}
-
-void
-InotifySource::Remove(unsigned wd) noexcept
-{
-	auto ifd = socket_event.GetFileDescriptor();
-	int ret = inotify_rm_watch(ifd.Get(), wd);
-	if (ret < 0 && errno != EINVAL)
-		FmtError(inotify_domain, "inotify_rm_watch() has failed: {}",
-			 strerror(errno));
-
-	/* EINVAL may happen here when the file has been deleted; the
-	   kernel seems to auto-unregister deleted files */
-}
diff --git a/src/db/update/InotifySource.hxx b/src/db/update/InotifySource.hxx
deleted file mode 100644
index 7073b5abf..000000000
--- a/src/db/update/InotifySource.hxx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef MPD_INOTIFY_SOURCE_HXX
-#define MPD_INOTIFY_SOURCE_HXX
-
-#include "event/PipeEvent.hxx"
-
-typedef void (*mpd_inotify_callback_t)(int wd, unsigned mask,
-				       const char *name, void *ctx);
-
-class InotifySource final {
-	PipeEvent socket_event;
-
-	mpd_inotify_callback_t callback;
-	void *callback_ctx;
-
-public:
-	/**
-	 * Creates a new inotify source and registers it in the
-	 * #EventLoop.
-	 *
-	 * Throws #std::system_error on error.
-	 *
-	 * @param callback a callback invoked for events received from
-	 * the kernel
-	 */
-	InotifySource(EventLoop &_loop,
-		      mpd_inotify_callback_t callback, void *ctx);
-
-	~InotifySource() noexcept {
-		socket_event.Close();
-	}
-
-	/**
-	 * Adds a path to the notify list.
-	 *
-	 * Throws #std::system_error on error.
-	 *
-	 * @return a watch descriptor
-	 */
-	int Add(const char *path_fs, unsigned mask);
-
-	/**
-	 * Removes a path from the notify list.
-	 *
-	 * @param wd the watch descriptor returned by mpd_inotify_source_add()
-	 */
-	void Remove(unsigned wd) noexcept;
-
-private:
-	void OnSocketReady(unsigned flags) noexcept;
-};
-
-#endif
diff --git a/src/db/update/InotifyUpdate.cxx b/src/db/update/InotifyUpdate.cxx
index 888682df9..c06225ba8 100644
--- a/src/db/update/InotifyUpdate.cxx
+++ b/src/db/update/InotifyUpdate.cxx
@@ -118,7 +118,7 @@ InotifyUpdate::Disable(WatchDirectory &directory) noexcept
 	for (WatchDirectory &child : directory.children)
 		Disable(child);
 
-	source.Remove(directory.descriptor);
+	inotify_event.RemoveWatch(directory.descriptor);
 }
 
 void
@@ -199,7 +199,8 @@ try {
 			continue;
 
 		try {
-			ret = source.Add(child_path_fs.c_str(), IN_MASK);
+			ret = inotify_event.AddWatch(child_path_fs.c_str(),
+						     IN_MASK);
 		} catch (...) {
 			FmtError(inotify_domain,
 				 "Failed to register {}: {}",
@@ -240,7 +241,7 @@ WatchDirectory::GetDepth() const noexcept
 inline
 InotifyUpdate::InotifyUpdate(EventLoop &loop, UpdateService &update,
 			     unsigned _max_depth)
-	:source(loop, InotifyCallback, this),
+	:inotify_event(loop, *this),
 	 queue(loop, update),
 	 max_depth(_max_depth)
 {
@@ -251,7 +252,7 @@ InotifyUpdate::~InotifyUpdate() noexcept = default;
 inline void
 InotifyUpdate::Start(Path path)
 {
-	int descriptor = source.Add(path.c_str(), IN_MASK);
+	int descriptor = inotify_event.AddWatch(path.c_str(), IN_MASK);
 
 	root = std::make_unique<WatchDirectory>(path, descriptor);
 	root->LoadExcludeList(path);
@@ -262,8 +263,7 @@ InotifyUpdate::Start(Path path)
 }
 
 void
-InotifyUpdate::InotifyCallback(int wd, unsigned mask,
-			       [[maybe_unused]] const char *name) noexcept
+InotifyUpdate::OnInotify(int wd, unsigned mask, const char *)
 {
 	auto i = directories.find(wd);
 	if (i == directories.end())
@@ -310,6 +310,12 @@ InotifyUpdate::InotifyCallback(int wd, unsigned mask,
 	}
 }
 
+void
+InotifyUpdate::OnInotifyError(std::exception_ptr error) noexcept
+{
+	LogError(error, "inotify error");
+}
+
 std::unique_ptr<InotifyUpdate>
 mpd_inotify_init(EventLoop &loop, Storage &storage, UpdateService &update,
 		 unsigned max_depth)
diff --git a/src/db/update/InotifyUpdate.hxx b/src/db/update/InotifyUpdate.hxx
index ad3783f99..cbff1979e 100644
--- a/src/db/update/InotifyUpdate.hxx
+++ b/src/db/update/InotifyUpdate.hxx
@@ -20,8 +20,8 @@
 #ifndef MPD_INOTIFY_UPDATE_HXX
 #define MPD_INOTIFY_UPDATE_HXX
 
-#include "InotifySource.hxx"
 #include "InotifyQueue.hxx"
+#include "event/InotifyEvent.hxx"
 
 #include <map>
 #include <memory>
@@ -33,8 +33,8 @@ struct WatchDirectory;
 /**
  * Glue code between InotifySource and InotifyQueue.
  */
-class InotifyUpdate {
-	InotifySource source;
+class InotifyUpdate final : InotifyHandler {
+	InotifyEvent inotify_event;
 	InotifyQueue queue;
 
 	const unsigned max_depth;
@@ -50,14 +50,6 @@ public:
 	void Start(Path path);
 
 private:
-	void InotifyCallback(int wd, unsigned mask, const char *name) noexcept;
-
-	static void InotifyCallback(int wd, unsigned mask,
-				    const char *name, void *ctx) noexcept {
-		auto &iu = *(InotifyUpdate *)ctx;
-		iu.InotifyCallback(wd, mask, name);
-	}
-
 	void AddToMap(WatchDirectory &directory) noexcept;
 	void RemoveFromMap(WatchDirectory &directory) noexcept;
 	void Disable(WatchDirectory &directory) noexcept;
@@ -66,6 +58,11 @@ private:
 	void RecursiveWatchSubdirectories(WatchDirectory &parent,
 					  Path path_fs,
 					  unsigned depth) noexcept;
+
+private:
+	/* virtual methods from class InotifyHandler */
+	void OnInotify(int wd, unsigned mask, const char *name) override;
+	void OnInotifyError(std::exception_ptr error) noexcept override;
 };
 
 /**
diff --git a/src/event/InotifyEvent.cxx b/src/event/InotifyEvent.cxx
new file mode 100644
index 000000000..609923d48
--- /dev/null
+++ b/src/event/InotifyEvent.cxx
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 CM4all GmbH
+ * All rights reserved.
+ *
+ * author: Max Kellermann <mk@cm4all.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "InotifyEvent.hxx"
+#include "system/Error.hxx"
+#include "io/UniqueFileDescriptor.hxx"
+
+#include <array>
+
+#include <limits.h>
+#include <sys/inotify.h>
+
+static UniqueFileDescriptor
+CreateInotify()
+{
+	int fd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
+	if (fd < 0)
+		throw MakeErrno("inotify_init1() failed");
+
+	return UniqueFileDescriptor(fd);
+}
+
+InotifyEvent::InotifyEvent(EventLoop &event_loop, InotifyHandler &_handler)
+	:event(event_loop, BIND_THIS_METHOD(OnInotifyReady),
+	       CreateInotify().Release()),
+	 handler(_handler)
+{
+	Enable();
+}
+
+InotifyEvent::~InotifyEvent() noexcept
+{
+	Close();
+}
+
+int
+InotifyEvent::AddWatch(const char *pathname, uint32_t mask)
+{
+	int wd = inotify_add_watch(event.GetFileDescriptor().Get(),
+				   pathname, mask);
+	if (wd < 0)
+		throw FormatErrno("inotify_add_watch('%s') failed", pathname);
+
+	return wd;
+}
+
+int
+InotifyEvent::AddModifyWatch(const char *pathname)
+{
+	return AddWatch(pathname, IN_MODIFY);
+}
+
+void
+InotifyEvent::RemoveWatch(int wd) noexcept
+{
+	inotify_rm_watch(event.GetFileDescriptor().Get(), wd);
+}
+
+inline void
+InotifyEvent::OnInotifyReady(unsigned) noexcept
+try {
+	std::array<std::byte, 4096> buffer;
+	static_assert(sizeof(buffer) >= sizeof(struct inotify_event) + NAME_MAX + 1,
+		      "inotify buffer too small");
+
+	ssize_t nbytes = event.GetFileDescriptor().Read(buffer.data(),
+							buffer.size());
+	if (nbytes <= 0) [[unlikely]] {
+		if (nbytes == 0)
+			throw std::runtime_error{"EOF from inotify"};
+
+		const int e = errno;
+		if (e == EAGAIN)
+			return;
+
+		throw MakeErrno(e, "Reading inotify failed");
+	}
+
+	const std::byte *p = buffer.data(), *const end = p + nbytes;
+
+	while (true) {
+		const size_t remaining = end - p;
+		const auto &ie = *(const struct inotify_event *)(const void *)p;
+		if (remaining < sizeof(ie) ||
+		    remaining < sizeof(ie) + ie.len)
+			break;
+
+		const char *name;
+		if (ie.len > 0 && ie.name[ie.len - 1] == 0)
+			name = ie.name;
+		else
+			name = nullptr;
+
+		handler.OnInotify(ie.wd, ie.mask, name);
+		p += sizeof(ie) + ie.len;
+	}
+} catch (...) {
+	Close();
+	handler.OnInotifyError(std::current_exception());
+}
diff --git a/src/event/InotifyEvent.hxx b/src/event/InotifyEvent.hxx
new file mode 100644
index 000000000..65b5d46a5
--- /dev/null
+++ b/src/event/InotifyEvent.hxx
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 CM4all GmbH
+ * All rights reserved.
+ *
+ * author: Max Kellermann <mk@cm4all.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "PipeEvent.hxx"
+
+#include <exception>
+
+/**
+ * Handler for #InotifyEvent.
+ */
+class InotifyHandler {
+public:
+	/**
+	 * An inotify event was received.
+	 *
+	 * @param wd the watch descriptor returned by
+	 * InotifyEvent::AddWatch().
+	 */
+	virtual void OnInotify(int wd, unsigned mask, const char *name) = 0;
+
+	/**
+	 * An (permanent) inotify error has occurred, and the
+	 * #InotifyEvent has been closed.
+	 */
+	virtual void OnInotifyError(std::exception_ptr error) noexcept = 0;
+};
+
+/**
+ * #EventLoop integration for Linux inotify.
+ */
+class InotifyEvent final {
+	PipeEvent event;
+
+	InotifyHandler &handler;
+
+public:
+	/**
+	 * Create an inotify file descriptor add register it in the
+	 * #EventLoop.
+	 *
+	 * Throws on error.
+	 */
+	InotifyEvent(EventLoop &event_loop, InotifyHandler &_handler);
+
+	~InotifyEvent() noexcept;
+
+	EventLoop &GetEventLoop() const noexcept {
+		return event.GetEventLoop();
+	}
+
+	/**
+	 * Re-enable polling the inotify file descriptor after it was
+	 * disabled by Disable().
+	 */
+	void Enable() noexcept {
+		event.ScheduleRead();
+	}
+
+	/**
+	 * Disable polling the inotify file descriptor.  Can be
+	 * re-enabled by Enable().
+	 */
+	void Disable() noexcept {
+		event.Cancel();
+	}
+
+	/**
+	 * Permanently close the inotify file descriptor.  Further
+	 * method calls not allowed after that.
+	 */
+	void Close() noexcept {
+		event.Close();
+	}
+
+	/**
+	 * Register a new path to be watched.
+	 *
+	 * Throws on error.
+	 *
+	 * @return a watch descriptor
+	 */
+	int AddWatch(const char *pathname, uint32_t mask);
+
+	/**
+	 * Wrapper for AddWatch(pathname, IN_MODIFY).
+	 */
+	int AddModifyWatch(const char *pathname);
+
+	/**
+	 * Stop watching the given watch descriptor.
+	 *
+	 * @param wd a watch descriptor returned by AddWatch()
+	 */
+	void RemoveWatch(int wd) noexcept;
+
+private:
+	void OnInotifyReady(unsigned) noexcept;
+};
diff --git a/src/event/meson.build b/src/event/meson.build
index c8b054e50..c20a38028 100644
--- a/src/event/meson.build
+++ b/src/event/meson.build
@@ -19,6 +19,10 @@ else
   event_sources += 'PollBackend.cxx'
 endif
 
+if enable_inotify
+  event_sources += 'InotifyEvent.cxx'
+endif
+
 event = static_library(
   'event',
   'SignalMonitor.cxx',
diff --git a/src/io/FileDescriptor.cxx b/src/io/FileDescriptor.cxx
index a7337db8c..72d37b981 100644
--- a/src/io/FileDescriptor.cxx
+++ b/src/io/FileDescriptor.cxx
@@ -44,7 +44,6 @@
 #ifdef __linux__
 #include <sys/eventfd.h>
 #include <sys/signalfd.h>
-#include <sys/inotify.h>
 #endif
 
 #ifndef O_NOCTTY
@@ -283,17 +282,6 @@ FileDescriptor::CreateSignalFD(const sigset_t *mask) noexcept
 	return true;
 }
 
-bool
-FileDescriptor::CreateInotify() noexcept
-{
-	int new_fd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
-	if (new_fd < 0)
-		return false;
-
-	fd = new_fd;
-	return true;
-}
-
 #endif
 
 bool
diff --git a/src/io/FileDescriptor.hxx b/src/io/FileDescriptor.hxx
index 1259edf11..43267d1b0 100644
--- a/src/io/FileDescriptor.hxx
+++ b/src/io/FileDescriptor.hxx
@@ -209,7 +209,6 @@ public:
 #ifdef __linux__
 	bool CreateEventFD(unsigned initval=0) noexcept;
 	bool CreateSignalFD(const sigset_t *mask) noexcept;
-	bool CreateInotify() noexcept;
 #endif
 
 	/**
diff --git a/test/meson.build b/test/meson.build
index 909057910..841e95447 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -133,8 +133,6 @@ if enable_inotify
     'run_inotify',
     'run_inotify.cxx',
     'ShutdownHandler.cxx',
-    '../src/db/update/InotifyDomain.cxx',
-    '../src/db/update/InotifySource.cxx',
     include_directories: inc,
     dependencies: [
       log_dep,
diff --git a/test/run_inotify.cxx b/test/run_inotify.cxx
index 77ddf32a1..3dff87333 100644
--- a/test/run_inotify.cxx
+++ b/test/run_inotify.cxx
@@ -18,7 +18,7 @@
  */
 
 #include "ShutdownHandler.hxx"
-#include "db/update/InotifySource.hxx"
+#include "event/InotifyEvent.hxx"
 #include "event/Loop.hxx"
 #include "Log.hxx"
 
@@ -33,18 +33,23 @@ static constexpr unsigned IN_MASK =
 	IN_ATTRIB|IN_CLOSE_WRITE|IN_CREATE|IN_DELETE|IN_DELETE_SELF
 	|IN_MOVE|IN_MOVE_SELF;
 
-static void
-my_inotify_callback([[maybe_unused]] int wd, unsigned mask,
-		    const char *name, [[maybe_unused]] void *ctx)
-{
-	printf("mask=0x%x name='%s'\n", mask, name);
-}
-
-struct Instance {
+struct Instance final : InotifyHandler {
 	EventLoop event_loop;
 	const ShutdownHandler shutdown_handler{event_loop};
 
-	InotifySource source{event_loop, my_inotify_callback, nullptr};
+	InotifyEvent inotify_event{event_loop, *this};
+
+	std::exception_ptr error;
+
+	/* virtual methods from class InotifyHandler */
+	void OnInotify(int, unsigned mask, const char *name) override {
+		printf("mask=0x%x name='%s'\n", mask, name);
+	}
+
+	void OnInotifyError(std::exception_ptr _error) noexcept override {
+		error = std::move(_error);
+		event_loop.Break();
+	}
 };
 
 int main(int argc, char **argv)
@@ -60,10 +65,13 @@ try {
 
 	Instance instance;
 
-	instance.source.Add(path, IN_MASK);
+	instance.inotify_event.AddWatch(path, IN_MASK);
 
 	instance.event_loop.Run();
 
+	if (instance.error)
+		std::rethrow_exception(instance.error);
+
 	return EXIT_SUCCESS;
 } catch (...) {
 	LogError(std::current_exception());