diff --git a/Cargo.lock b/Cargo.lock
index 4dba285..1408959 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,10 +17,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
 [[package]]
 name = "apache_prometheus_exporter"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "hyper",
  "notify",
  "path-slash",
diff --git a/Cargo.toml b/Cargo.toml
index 06ab33c..02f48ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ lto = true
 codegen-units = 1
 
 [dependencies]
+anyhow = "1.0.75"
 hyper = { version = "0.14.27", default-features = false, features = ["http1", "server", "runtime"] }
 notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"] }
 path-slash = "0.2.1"
diff --git a/src/logs/log_file_pattern.rs b/src/logs/log_file_pattern.rs
index a9b545a..3685454 100644
--- a/src/logs/log_file_pattern.rs
+++ b/src/logs/log_file_pattern.rs
@@ -1,9 +1,9 @@
-use std::{env, io};
-use std::env::VarError;
 use std::fs::DirEntry;
+use std::io;
 use std::io::ErrorKind;
 use std::path::{Path, PathBuf};
 
+use anyhow::{anyhow, bail, Result};
 use path_slash::PathExt;
 
 /// Reads and parses an environment variable that determines the path and file name pattern of log files.
@@ -13,34 +13,22 @@ use path_slash::PathExt;
 /// 1. A simple path to a file.
 /// 2. A path with a wildcard anywhere in the file name.
 /// 3. A path with a standalone wildcard component (i.e. no prefix or suffix in the folder name).
-pub fn parse_log_file_pattern_from_env(variable_name: &str) -> Result<LogFilePattern, String> {
-	match env::var(variable_name) {
-		Ok(str) => {
-			let pattern_str = Path::new(&str).to_slash().ok_or(format!("Environment variable {} contains an invalid path.", variable_name))?;
-			parse_log_file_pattern_from_str(&pattern_str)
-		}
-		Err(err) => match err {
-			VarError::NotPresent => Err(format!("Environment variable {} must be set.", variable_name)),
-			VarError::NotUnicode(_) => Err(format!("Environment variable {} contains invalid characters.", variable_name))
-		}
-	}
-}
-
-fn parse_log_file_pattern_from_str(pattern_str: &str) -> Result<LogFilePattern, String> {
-	if pattern_str.trim().is_empty() {
-		return Err(String::from("Path is empty."));
+pub fn parse_log_file_pattern_from_str(pattern: &str) -> Result<LogFilePattern> {
+	let pattern = Path::new(pattern).to_slash().ok_or_else(|| anyhow!("Path is invalid"))?;
+	if pattern.trim().is_empty() {
+		bail!("Path is empty");
 	}
 	
-	if let Some((left, right)) = pattern_str.split_once('*') {
+	if let Some((left, right)) = pattern.split_once('*') {
 		parse_log_file_pattern_split_on_wildcard(left, right)
 	} else {
-		Ok(LogFilePattern::WithoutWildcard(pattern_str.to_string()))
+		Ok(LogFilePattern::WithoutWildcard(pattern.to_string()))
 	}
 }
 
-fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<LogFilePattern, String> {
+fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<LogFilePattern> {
 	if left.contains('*') || right.contains('*') {
-		return Err(String::from("Path has too many wildcards."));
+		bail!("Path has too many wildcards");
 	}
 	
 	if left.ends_with('/') && right.starts_with('/') {
@@ -51,7 +39,7 @@ fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<L
 	}
 	
 	if right.contains('/') {
-		return Err(String::from("Path has a folder wildcard with a prefix or suffix."));
+		bail!("Path has a folder wildcard with a prefix or suffix");
 	}
 	
 	if let Some((folder_path, file_name_prefix)) = left.rsplit_once('/') {
@@ -122,10 +110,7 @@ impl LogFilePattern {
 	}
 	
 	fn search_without_wildcard(path_str: &String) -> Result<Vec<LogFilePath>, io::Error> {
-		let path = Path::new(path_str);
-		let is_valid = path.is_file() || matches!(path.parent(), Some(parent) if parent.is_dir());
-		
-		if is_valid {
+		if Path::new(path_str).is_file() {
 			Ok(vec![LogFilePath::with_empty_label(path_str)])
 		} else {
 			Err(io::Error::from(ErrorKind::NotFound))
@@ -182,23 +167,23 @@ mod tests {
 	
 	#[test]
 	fn empty_path() {
-		assert!(matches!(parse_log_file_pattern_from_str(""), Err(err) if err == "Path is empty."));
-		assert!(matches!(parse_log_file_pattern_from_str("  "), Err(err) if err == "Path is empty."));
+		assert!(matches!(parse_log_file_pattern_from_str(""), Err(err) if err.to_string() == "Path is empty"));
+		assert!(matches!(parse_log_file_pattern_from_str("  "), Err(err) if err.to_string() == "Path is empty"));
 	}
 	
 	#[test]
 	fn too_many_wildcards() {
-		assert!(matches!(parse_log_file_pattern_from_str("/path/*/to/files/*.log"), Err(err) if err == "Path has too many wildcards."));
+		assert!(matches!(parse_log_file_pattern_from_str("/path/*/to/files/*.log"), Err(err) if err.to_string() == "Path has too many wildcards"));
 	}
 	
 	#[test]
 	fn folder_wildcard_with_prefix_not_supported() {
-		assert!(matches!(parse_log_file_pattern_from_str("/path/*abc/to/files/access.log"), Err(err) if err == "Path has a folder wildcard with a prefix or suffix."));
+		assert!(matches!(parse_log_file_pattern_from_str("/path/*abc/to/files/access.log"), Err(err) if err.to_string() == "Path has a folder wildcard with a prefix or suffix"));
 	}
 	
 	#[test]
 	fn folder_wildcard_with_suffix_not_supported() {
-		assert!(matches!(parse_log_file_pattern_from_str("/path/abc*/to/files/access.log"), Err(err) if err == "Path has a folder wildcard with a prefix or suffix."));
+		assert!(matches!(parse_log_file_pattern_from_str("/path/abc*/to/files/access.log"), Err(err) if err.to_string() == "Path has a folder wildcard with a prefix or suffix"));
 	}
 	
 	#[test]
diff --git a/src/logs/log_file_watcher.rs b/src/logs/log_file_watcher.rs
index 1dab437..5e3439f 100644
--- a/src/logs/log_file_watcher.rs
+++ b/src/logs/log_file_watcher.rs
@@ -2,6 +2,7 @@ use std::cmp::max;
 use std::path::PathBuf;
 use std::sync::Arc;
 
+use anyhow::{anyhow, bail, Context, Result};
 use notify::{Event, EventKind};
 use notify::event::{CreateKind, ModifyKind};
 use tokio::fs::File;
@@ -50,10 +51,9 @@ impl LogWatcherConfiguration {
 		self.files.push((path, metadata));
 	}
 	
-	pub async fn start(self, metrics: &Metrics) -> bool {
+	pub async fn start(self, metrics: &Metrics) -> Result<()> {
 		if self.files.is_empty() {
-			println!("[LogWatcher] No log files provided.");
-			return false;
+			bail!("No log files provided");
 		}
 		
 		println!("[LogWatcher] Watching {} access log file(s) and {} error log file(s).", self.count_files_of_kind(LogFileKind::Access), self.count_files_of_kind(LogFileKind::Error));
@@ -73,32 +73,16 @@ impl LogWatcherConfiguration {
 			prepared_files.push(PreparedFile { path, metadata, fs_event_receiver });
 		}
 		
-		let fs_watcher = match FsWatcher::new(fs_callbacks) {
-			Ok(fs_watcher) => fs_watcher,
-			Err(e) => {
-				println!("[LogWatcher] Error creating filesystem watcher: {}", e);
-				return false;
-			}
-		};
+		let fs_watcher = FsWatcher::new(fs_callbacks).context("Could not create filesystem watcher")?;
 		
 		for file in &prepared_files {
 			let file_path = &file.path;
 			if !file_path.is_absolute() {
-				println!("[LogWatcher] Error creating filesystem watcher, path is not absolute: {}", file_path.to_string_lossy());
-				return false;
+				bail!("Path is not absolute: {}", file_path.to_string_lossy());
 			}
 			
-			let parent_path = if let Some(parent) = file_path.parent() {
-				parent
-			} else {
-				println!("[LogWatcher] Error creating filesystem watcher for parent directory of file \"{}\", parent directory does not exist", file_path.to_string_lossy());
-				return false;
-			};
-			
-			if let Err(e) = fs_watcher.watch(parent_path).await {
-				println!("[LogWatcher] Error creating filesystem watcher for directory \"{}\": {}", parent_path.to_string_lossy(), e);
-				return false;
-			}
+			let parent_path = file_path.parent().ok_or_else(|| anyhow!("Path has no parent: {}", file_path.to_string_lossy()))?;
+			fs_watcher.watch(parent_path).await.with_context(|| format!("Could not create filesystem watcher for directory: {}", parent_path.to_string_lossy()))?;
 		}
 		
 		let fs_watcher = Arc::new(fs_watcher);
@@ -108,15 +92,13 @@ impl LogWatcherConfiguration {
 			let _ = metrics.requests_total.get_or_create(&label_set);
 			let _ = metrics.errors_total.get_or_create(&label_set);
 			
-			let log_watcher = match LogWatcher::create(file.path, file.metadata, metrics.clone(), Arc::clone(&fs_watcher), file.fs_event_receiver).await {
-				Some(log_watcher) => log_watcher,
-				None => return false,
-			};
+			let log_watcher = LogWatcher::create(file.path.clone(), file.metadata, metrics.clone(), Arc::clone(&fs_watcher), file.fs_event_receiver);
+			let log_watcher = log_watcher.await.with_context(|| format!("Could not watch log file: {}", file.path.to_string_lossy()))?;
 			
 			tokio::spawn(log_watcher.watch());
 		}
 		
-		true
+		Ok(())
 	}
 }
 
@@ -127,14 +109,10 @@ struct LogWatcher {
 }
 
 impl LogWatcher {
-	async fn create(path: PathBuf, metadata: LogFileMetadata, metrics: Metrics, fs_watcher: Arc<FsWatcher>, fs_event_receiver: Receiver<Event>) -> Option<Self> {
-		let state = match LogWatchingState::initialize(path.clone(), fs_watcher).await {
-			Some(state) => state,
-			None => return None,
-		};
-		
+	async fn create(path: PathBuf, metadata: LogFileMetadata, metrics: Metrics, fs_watcher: Arc<FsWatcher>, fs_event_receiver: Receiver<Event>) -> Result<Self> {
+		let state = LogWatchingState::initialize(path.clone(), fs_watcher).await?;
 		let processor = LogLineProcessor { path, metadata, metrics };
-		Some(LogWatcher { state, processor, fs_event_receiver })
+		Ok(LogWatcher { state, processor, fs_event_receiver })
 	}
 	
 	async fn watch(mut self) {
@@ -176,8 +154,11 @@ impl LogWatcher {
 						}
 						
 						self.state = match self.state.reinitialize().await {
-							Some(state) => state,
-							None => break 'read_loop,
+							Ok(state) => state,
+							Err(e) => {
+								println!("Could not re-watch log file \"{}\": {}", path.to_string_lossy(), e);
+								break 'read_loop;
+							}
 						};
 						
 						continue 'read_loop;
@@ -222,24 +203,16 @@ struct LogWatchingState {
 impl LogWatchingState {
 	const DEFAULT_BUFFER_CAPACITY: usize = 1024 * 4;
 	
-	async fn initialize(path: PathBuf, fs_watcher: Arc<FsWatcher>) -> Option<LogWatchingState> {
-		if let Err(e) = fs_watcher.watch(&path).await {
-			println!("[LogWatcher] Error creating filesystem watcher for file \"{}\": {}", path.to_string_lossy(), e);
-			return None;
-		}
+	async fn initialize(path: PathBuf, fs_watcher: Arc<FsWatcher>) -> Result<LogWatchingState> {
+		fs_watcher.watch(&path).await.context("Could not create filesystem watcher")?;
 		
-		let lines = match File::open(&path).await {
-			Ok(file) => BufReader::with_capacity(Self::DEFAULT_BUFFER_CAPACITY, file).lines(),
-			Err(e) => {
-				println!("[LogWatcher] Error opening file \"{}\": {}", path.to_string_lossy(), e);
-				return None;
-			}
-		};
+		let file = File::open(&path).await.context("Could not open file")?;
+		let lines = BufReader::with_capacity(Self::DEFAULT_BUFFER_CAPACITY, file).lines();
 		
-		Some(LogWatchingState { path, lines, fs_watcher })
+		Ok(LogWatchingState { path, lines, fs_watcher })
 	}
 	
-	async fn reinitialize(self) -> Option<LogWatchingState> {
+	async fn reinitialize(self) -> Result<LogWatchingState> {
 		LogWatchingState::initialize(self.path, self.fs_watcher).await
 	}
 }
diff --git a/src/logs/mod.rs b/src/logs/mod.rs
index ee5a320..a497704 100644
--- a/src/logs/mod.rs
+++ b/src/logs/mod.rs
@@ -1,6 +1,11 @@
+use std::env;
+use std::env::VarError;
+
+use anyhow::{anyhow, bail, Context, Result};
+
 use log_file_watcher::{LogFileKind, LogWatcherConfiguration};
 
-use crate::logs::log_file_pattern::{LogFilePath, parse_log_file_pattern_from_env};
+use crate::logs::log_file_pattern::{LogFilePath, parse_log_file_pattern_from_str};
 use crate::metrics::Metrics;
 
 mod access_log_parser;
@@ -8,36 +13,27 @@ mod filesystem_watcher;
 mod log_file_pattern;
 mod log_file_watcher;
 
-pub fn find_log_files(environment_variable_name: &str, log_kind: &str) -> Option<Vec<LogFilePath>> {
-	let log_file_pattern = match parse_log_file_pattern_from_env(environment_variable_name) {
-		Ok(pattern) => pattern,
-		Err(error) => {
-			println!("Error: {}", error);
-			return None;
-		}
-	};
+pub fn find_log_files(environment_variable_name: &str, log_kind: &str) -> Result<Vec<LogFilePath>> {
+	let log_file_pattern_str = env::var(environment_variable_name).map_err(|err| match err {
+		VarError::NotPresent => anyhow!("Environment variable {} must be set", environment_variable_name),
+		VarError::NotUnicode(_) => anyhow!("Environment variable {} contains invalid characters", environment_variable_name)
+	})?;
 	
-	let log_files = match log_file_pattern.search() {
-		Ok(files) => files,
-		Err(error) => {
-			println!("Error searching {} files: {}", log_kind, error);
-			return None;
-		}
-	};
+	let log_file_pattern = parse_log_file_pattern_from_str(&log_file_pattern_str).with_context(|| format!("Could not parse pattern: {}", log_file_pattern_str))?;
+	let log_files = log_file_pattern.search().with_context(|| format!("Could not search files: {}", log_file_pattern_str))?;
 	
 	if log_files.is_empty() {
-		println!("Found no matching {} files.", log_kind);
-		return None;
+		bail!("No files match pattern: {}", log_file_pattern_str);
 	}
 	
 	for log_file in &log_files {
 		println!("Found {} file: {} (label \"{}\")", log_kind, log_file.path.display(), log_file.label);
 	}
 	
-	Some(log_files)
+	Ok(log_files)
 }
 
-pub async fn start_log_watcher(access_log_files: Vec<LogFilePath>, error_log_files: Vec<LogFilePath>, metrics: Metrics) -> bool {
+pub async fn start_log_watcher(access_log_files: Vec<LogFilePath>, error_log_files: Vec<LogFilePath>, metrics: Metrics) -> Result<()> {
 	let mut watcher = LogWatcherConfiguration::new();
 	
 	for log_file in access_log_files.into_iter() {
diff --git a/src/main.rs b/src/main.rs
index de107b1..98a1d22 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,9 @@
 use std::env;
 use std::net::{IpAddr, SocketAddr};
-use std::process::ExitCode;
 use std::str::FromStr;
 use std::sync::Mutex;
 
+use anyhow::{anyhow, Context};
 use tokio::signal;
 
 use crate::metrics::Metrics;
@@ -17,49 +17,22 @@ const ACCESS_LOG_FILE_PATTERN: &str = "ACCESS_LOG_FILE_PATTERN";
 const ERROR_LOG_FILE_PATTERN: &str = "ERROR_LOG_FILE_PATTERN";
 
 #[tokio::main(flavor = "current_thread")]
-async fn main() -> ExitCode {
+async fn main() -> anyhow::Result<()> {
 	let host = env::var("HTTP_HOST").unwrap_or(String::from("127.0.0.1"));
-	let bind_ip = match IpAddr::from_str(&host) {
-		Ok(addr) => addr,
-		Err(_) => {
-			println!("Invalid HTTP host: {}", host);
-			return ExitCode::FAILURE;
-		}
-	};
+	let bind_ip = IpAddr::from_str(&host).map_err(|_| anyhow!("Invalid HTTP host: {}", host))?;
 	
 	println!("Initializing exporter...");
 	
-	let access_log_files = match logs::find_log_files(ACCESS_LOG_FILE_PATTERN, "access log") {
-		Some(files) => files,
-		None => return ExitCode::FAILURE,
-	};
-	
-	let error_log_files = match logs::find_log_files(ERROR_LOG_FILE_PATTERN, "error log") {
-		Some(files) => files,
-		None => return ExitCode::FAILURE,
-	};
-	
-	let server = match WebServer::try_bind(SocketAddr::new(bind_ip, 9240)) {
-		Some(server) => server,
-		None => return ExitCode::FAILURE
-	};
+	let access_log_files = logs::find_log_files(ACCESS_LOG_FILE_PATTERN, "access log").context("Could not find access log files")?;
+	let error_log_files = logs::find_log_files(ERROR_LOG_FILE_PATTERN, "error log").context("Could not find error log files")?;
 	
+	let server = WebServer::try_bind(SocketAddr::new(bind_ip, 9240)).context("Could not configure web server")?;
 	let (metrics_registry, metrics) = Metrics::new();
 	
-	if !logs::start_log_watcher(access_log_files, error_log_files, metrics).await {
-		return ExitCode::FAILURE;
-	}
-	
+	logs::start_log_watcher(access_log_files, error_log_files, metrics).await.context("Could not start watching logs")?;
 	tokio::spawn(server.serve(Mutex::new(metrics_registry)));
 	
-	match signal::ctrl_c().await {
-		Ok(_) => {
-			println!("Received CTRL-C, shutting down...");
-			ExitCode::SUCCESS
-		}
-		Err(e) => {
-			println!("Error registering CTRL-C handler: {}", e);
-			ExitCode::FAILURE
-		}
-	}
+	signal::ctrl_c().await.with_context(|| "Could not register CTRL-C handler")?;
+	println!("Received CTRL-C, shutting down...");
+	Ok(())
 }
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 5fb2048..12a9234 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -2,6 +2,7 @@ use std::net::SocketAddr;
 use std::sync::{Arc, Mutex};
 use std::time::Duration;
 
+use anyhow::Context;
 use hyper::{Body, Error, Method, Request, Response, Server, StatusCode};
 use hyper::http::Result;
 use hyper::server::Builder;
@@ -19,23 +20,17 @@ pub struct WebServer {
 
 impl WebServer {
 	//noinspection HttpUrlsUsage
-	pub fn try_bind(addr: SocketAddr) -> Option<WebServer> {
+	pub fn try_bind(addr: SocketAddr) -> anyhow::Result<WebServer> {
 		println!("[WebServer] Starting web server on {0} with metrics endpoint: http://{0}/metrics", addr);
-		let builder = match Server::try_bind(&addr) {
-			Ok(builder) => builder,
-			Err(e) => {
-				println!("[WebServer] Could not bind to {}: {}", addr, e);
-				return None;
-			}
-		};
 		
+		let builder = Server::try_bind(&addr).with_context(|| format!("Could not bind to {}", addr))?;
 		let builder = builder.tcp_keepalive(Some(Duration::from_secs(60)));
 		let builder = builder.http1_only(true);
 		let builder = builder.http1_keepalive(true);
 		let builder = builder.http1_max_buf_size(MAX_BUFFER_SIZE);
 		let builder = builder.http1_header_read_timeout(Duration::from_secs(10));
 		
-		Some(WebServer { builder })
+		Ok(WebServer { builder })
 	}
 	
 	pub async fn serve(self, metrics_registry: Mutex<Registry>) {