From 6fc3fd8aaffc5e1499868dd478c27632c4b8ef4f Mon Sep 17 00:00:00 2001
From: Eduardo Trujillo <ed@chromabits.com>
Date: Sun, 19 May 2024 21:25:22 +0000
Subject: [PATCH] refactor: Migrate from snafu to thiserror

---
 Cargo.lock                | 29 +-------------
 Cargo.toml                |  2 +-
 src/bundle/mod.rs         | 79 +++++++++++++++++++++++++++---------
 src/bundle/packager.rs    | 15 +++++--
 src/bundle/poller.rs      | 18 +++++++--
 src/bundle/s3/packager.rs | 49 ++++++++++++-----------
 src/bundle/s3/poller.rs   | 28 ++++++++-----
 src/cli/bundle.rs         |  2 +-
 src/cli/serve.rs          | 49 ++++++++++++++---------
 src/config.rs             | 22 ++++++----
 src/files/path.rs         | 29 +++++++++-----
 src/files/path_context.rs | 55 +++++++++++++++----------
 src/files/pathbuf.rs      | 70 ++++++++++++++++----------------
 src/files/service.rs      | 84 +++++++++++++++++++++++----------------
 src/main.rs               | 38 ++++++++++++------
 src/server.rs             | 32 ++++++++++-----
 src/stats.rs              | 43 ++++++++++++++------
 17 files changed, 390 insertions(+), 254 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index cf1e401..4a76f70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -932,12 +932,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "doc-comment"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
-
 [[package]]
 name = "either"
 version = "1.12.0"
@@ -1032,7 +1026,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "snafu",
+ "thiserror",
  "tokio",
  "tokio-stream",
  "tokio-tar",
@@ -2511,27 +2505,6 @@ version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
 
-[[package]]
-name = "snafu"
-version = "0.6.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7"
-dependencies = [
- "doc-comment",
- "snafu-derive",
-]
-
-[[package]]
-name = "snafu-derive"
-version = "0.6.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "socket2"
 version = "0.5.7"
diff --git a/Cargo.toml b/Cargo.toml
index 03e9fac..111461f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,7 +37,7 @@ rusoto_s3 = "0.48.0"
 serde = "1.0"
 serde_derive = "1.0"
 serde_json = "1.0"
-snafu = "0.6.8"
+thiserror = "1.0"
 tokio-stream = "0.1"
 toml = "0.5"
 url = "2.5"
diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs
index 191aa17..8e909f1 100644
--- a/src/bundle/mod.rs
+++ b/src/bundle/mod.rs
@@ -2,8 +2,8 @@ use crate::config::Config;
 use collective::rundir::{self, RunDir};
 use s3::packager::S3BundlePackager;
 use serde::Serialize;
-use snafu::{ResultExt, Snafu};
 use std::{path::PathBuf, sync::Arc};
+use thiserror::Error;
 use tokio::{
   sync::RwLock,
   time::{interval, Duration},
@@ -16,19 +16,42 @@ pub mod packager;
 pub mod poller;
 pub mod s3;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
-  InitRunDir { source: rundir::RunDirError },
-  DeinitRunDir { source: rundir::RunDirError },
+#[derive(Debug, Error)]
+pub enum BundleError {
+  #[error("Unable to initialzie rundir")]
+  InitRunDir {
+    #[source]
+    source: rundir::RunDirError,
+  },
+  #[error("Unable to deinitialize rundir")]
+  DeinitRunDir {
+    #[source]
+    source: rundir::RunDirError,
+  },
+  #[error("Unable to read lock")]
   LockRead,
+  #[error("Unable to write lock")]
   LockWrite,
-  AttachSignalHook { source: std::io::Error },
+  #[error("Unable to attach signal hook")]
+  AttachSignalHook {
+    #[source]
+    source: std::io::Error,
+  },
+  #[error("Got missing eTag")]
   MissingETag,
-  PollError { source: poller::Error },
-  SubDirError { source: rundir::RunDirError },
+  #[error(transparent)]
+  PollError {
+    #[from]
+    source: poller::PollerError,
+  },
+  #[error("Unable to set up subdir")]
+  SubDirError {
+    #[source]
+    source: rundir::RunDirError,
+  },
 }
 
-type Result<T, E = Error> = std::result::Result<T, E>;
+type Result<T, E = BundleError> = std::result::Result<T, E>;
 
 #[derive(Copy, Clone, Debug, Serialize, PartialEq)]
 pub enum UnbundlerStatus {
@@ -79,7 +102,7 @@ impl Bundler {
     Bundler { packager }
   }
 
-  pub async fn package(&self, path: PathBuf) -> Result<(), packager::Error> {
+  pub async fn package(&self, path: PathBuf) -> Result<(), packager::PackagerError> {
     log::info!("Packaging bundle at {}", path.display());
 
     self.packager.generate(path).await
@@ -188,8 +211,16 @@ impl Unbundler {
 
     let mut state = self.state.write().await;
 
-    state.rundir.initialize().context(InitRunDir)?;
-    state.temp_dir = Some(state.rundir.create_subdir("temp").context(InitRunDir)?);
+    state
+      .rundir
+      .initialize()
+      .map_err(|source| BundleError::InitRunDir { source })?;
+    state.temp_dir = Some(
+      state
+        .rundir
+        .create_subdir("temp")
+        .map_err(|source| BundleError::InitRunDir { source })?,
+    );
 
     state.status = UnbundlerStatus::Initialized;
 
@@ -231,8 +262,7 @@ impl Unbundler {
         Err(err)
       }
       res => res,
-    }
-    .context(PollError)?;
+    }?;
 
     match result {
       poller::PollResult::Skip => {
@@ -261,7 +291,11 @@ impl Unbundler {
       poller::PollResult::UpdateReady { etag } => {
         let mut state = self.state.write().await;
 
-        if state.rundir.subdir_exists(&etag).context(SubDirError)? {
+        if state
+          .rundir
+          .subdir_exists(&etag)
+          .map_err(|source| BundleError::SubDirError { source })?
+        {
           log::warn!("Unbundler: Skipping update. Subdir already exists.");
 
           return Ok(());
@@ -271,7 +305,10 @@ impl Unbundler {
           etag: Some(etag.clone()),
         });
 
-        let newdir = state.rundir.create_subdir(&etag).context(SubDirError)?;
+        let newdir = state
+          .rundir
+          .create_subdir(&etag)
+          .map_err(|source| BundleError::SubDirError { source })?;
 
         let result = self.poller.retrieve(&etag, newdir.clone()).await;
 
@@ -281,7 +318,10 @@ impl Unbundler {
           state.staging_bundle = None;
           state.status = UnbundlerStatus::Ready;
 
-          state.rundir.remove_subdir_all(&etag).context(SubDirError)?;
+          state
+            .rundir
+            .remove_subdir_all(&etag)
+            .map_err(|source| BundleError::SubDirError { source })?;
 
           return Ok(());
         }
@@ -304,7 +344,10 @@ impl Unbundler {
     let mut state = self.state.write().await;
 
     state.status = UnbundlerStatus::Idle;
-    state.rundir.cleanup().context(DeinitRunDir)?;
+    state
+      .rundir
+      .cleanup()
+      .map_err(|source| BundleError::DeinitRunDir { source })?;
 
     Ok(())
   }
diff --git a/src/bundle/packager.rs b/src/bundle/packager.rs
index 6ee1aed..9e8fb84 100644
--- a/src/bundle/packager.rs
+++ b/src/bundle/packager.rs
@@ -1,15 +1,22 @@
 use async_trait::async_trait;
-use snafu::Snafu;
 use std::path::PathBuf;
+use thiserror::Error;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum PackagerError {
+  #[error("Internal packager error")]
   InternalBundlePackagerError {
+    #[from]
     source: Box<dyn std::error::Error + Sync + Send>,
   },
+  #[error(transparent)]
+  IOError {
+    #[from]
+    source: std::io::Error,
+  },
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
+pub type Result<T, E = PackagerError> = std::result::Result<T, E>;
 
 #[async_trait(?Send)]
 pub trait BundlePackager {
diff --git a/src/bundle/poller.rs b/src/bundle/poller.rs
index e4f5d6f..861df3e 100644
--- a/src/bundle/poller.rs
+++ b/src/bundle/poller.rs
@@ -1,24 +1,34 @@
 use super::Bundle;
 use async_trait::async_trait;
-use snafu::Snafu;
 use std::path::PathBuf;
+use thiserror::Error;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum PollerError {
+  #[error("Bundle ID is missing")]
   MissingBundleID,
+  #[error("Internal poller error")]
   InternalPollerError {
+    #[from]
     source: Box<dyn std::error::Error + Sync + Send>,
   },
+  #[error(
+    "Original Bundle ID {} does not match Current ID {}",
+    original_id,
+    current_id
+  )]
   MismatchedBundleID {
     original_id: String,
     current_id: String,
   },
+  #[error("Unable to unpack bundle")]
   UnpackError {
+    #[source]
     source: std::io::Error,
   },
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
+pub type Result<T, E = PollerError> = std::result::Result<T, E>;
 
 pub enum PollResult {
   Skip,
diff --git a/src/bundle/s3/packager.rs b/src/bundle/s3/packager.rs
index 248127a..7526f4e 100644
--- a/src/bundle/s3/packager.rs
+++ b/src/bundle/s3/packager.rs
@@ -1,11 +1,11 @@
-use crate::bundle::packager::{BundlePackager, Error, Result};
+use crate::bundle::packager::{BundlePackager, PackagerError, Result};
 use async_compression::tokio::write::GzipEncoder;
 use async_trait::async_trait;
 use futures_util::TryStreamExt;
 use rusoto_core::RusotoError;
 use rusoto_s3::{PutObjectError, PutObjectRequest, S3Client, StreamingBody, S3};
-use snafu::{ResultExt, Snafu};
 use std::path::PathBuf;
+use thiserror::Error;
 use tokio::{
   fs::{File, OpenOptions},
   io::AsyncWriteExt,
@@ -13,15 +13,23 @@ use tokio::{
 use tokio_tar::Builder as TarBuilder;
 use tokio_util::codec::{BytesCodec, FramedRead};
 
-#[derive(Snafu, Debug)]
+#[derive(Debug, Error)]
 pub enum InternalError {
-  S3PutObjectError { source: RusotoError<PutObjectError> },
-  IOError { source: std::io::Error },
+  #[error(transparent)]
+  S3PutObjectError {
+    #[from]
+    source: RusotoError<PutObjectError>,
+  },
+  #[error(transparent)]
+  IOError {
+    #[from]
+    source: std::io::Error,
+  },
 }
 
-impl From<InternalError> for Error {
+impl From<InternalError> for PackagerError {
   fn from(err: InternalError) -> Self {
-    Error::InternalBundlePackagerError {
+    PackagerError::InternalBundlePackagerError {
       source: Box::new(err),
     }
   }
@@ -66,24 +74,21 @@ impl BundlePackager for S3BundlePackager {
       .write(true)
       .read(true)
       .open("bundle.tar.gz")
-      .await
-      .context(IOError)?;
-    let tar_gz_file = File::create("bundle.tar").await.context(IOError)?;
+      .await?;
+    let tar_gz_file = File::create("bundle.tar").await?;
 
     let mut builder = TarBuilder::new(tar_file);
 
-    builder.append_dir_all(".", path).await.context(IOError)?;
-    let mut tar_file = builder.into_inner().await.context(IOError)?;
+    builder.append_dir_all(".", path).await?;
+    let mut tar_file = builder.into_inner().await?;
 
     let mut encoder = GzipEncoder::new(tar_gz_file);
 
-    tokio::io::copy(&mut tar_file, &mut encoder)
-      .await
-      .context(IOError)?;
+    tokio::io::copy(&mut tar_file, &mut encoder).await?;
 
-    encoder.shutdown().await.context(IOError)?;
+    encoder.shutdown().await?;
 
-    let tar_gz_file = File::open("bundle.tar.gz").await.context(IOError)?;
+    let tar_gz_file = File::open("bundle.tar.gz").await?;
 
     let final_stream_of_bytes = FramedRead::new(tar_gz_file, BytesCodec::new());
 
@@ -106,7 +111,7 @@ impl BundlePackager for S3BundlePackager {
       .client
       .put_object(request)
       .await
-      .context(S3PutObjectError)?;
+      .map_err(|source| InternalError::S3PutObjectError { source })?;
 
     log::info!(
       "S3BundlePackager: Bundle uploaded. ETag: {}",
@@ -115,12 +120,8 @@ impl BundlePackager for S3BundlePackager {
 
     log::info!("S3BundlePackager: Removing temporary files...");
 
-    tokio::fs::remove_file("bundle.tar")
-      .await
-      .context(IOError)?;
-    tokio::fs::remove_file("bundle.tar.gz")
-      .await
-      .context(IOError)?;
+    tokio::fs::remove_file("bundle.tar").await?;
+    tokio::fs::remove_file("bundle.tar.gz").await?;
 
     Ok(())
   }
diff --git a/src/bundle/s3/poller.rs b/src/bundle/s3/poller.rs
index 8a2ef57..d52fafa 100644
--- a/src/bundle/s3/poller.rs
+++ b/src/bundle/s3/poller.rs
@@ -1,5 +1,5 @@
 use crate::bundle::{
-  poller::{BundlePoller, Error, PollResult, Result},
+  poller::{BundlePoller, PollResult, PollerError, Result},
   Bundle,
 };
 
@@ -8,25 +8,31 @@ use rusoto_core::RusotoError;
 use rusoto_s3::{
   GetObjectError, GetObjectRequest, HeadObjectError, HeadObjectRequest, S3Client, S3,
 };
-use snafu::{ResultExt, Snafu};
 use std::path::PathBuf;
+use thiserror::Error;
 use tokio_tar::Archive;
 
-#[derive(Snafu, Debug)]
+#[derive(Debug, Error)]
 pub enum InternalError {
+  #[error(transparent)]
   S3HeadObjectError {
+    #[from]
     source: RusotoError<HeadObjectError>,
   },
+  #[error(transparent)]
   S3GetObjectError {
+    #[from]
     source: RusotoError<GetObjectError>,
   },
+  #[error("Missing body")]
   S3MissingBody,
+  #[error("Missing eTag")]
   S3MissingETag,
 }
 
-impl From<InternalError> for Error {
+impl From<InternalError> for PollerError {
   fn from(err: InternalError) -> Self {
-    Error::InternalPollerError {
+    PollerError::InternalPollerError {
       source: Box::new(err),
     }
   }
@@ -78,7 +84,7 @@ impl BundlePoller for S3BundlePoller {
           .client
           .head_object(head_request)
           .await
-          .context(S3HeadObjectError)?;
+          .map_err(|source| InternalError::S3HeadObjectError { source })?;
 
         if head_response.e_tag == active_bundle.etag {
           log::info!(
@@ -97,7 +103,7 @@ impl BundlePoller for S3BundlePoller {
       } else {
         log::error!("S3BundlePoller: Expected the active bundle to have a valid ETag.");
 
-        return Err(Error::MissingBundleID);
+        return Err(PollerError::MissingBundleID);
       }
     }
 
@@ -112,7 +118,7 @@ impl BundlePoller for S3BundlePoller {
       .client
       .head_object(head_request)
       .await
-      .context(S3HeadObjectError)?;
+      .map_err(|source| InternalError::S3HeadObjectError { source })?;
 
     Ok(PollResult::UpdateReady {
       etag: head_response.e_tag.ok_or(InternalError::S3MissingETag)?,
@@ -132,14 +138,14 @@ impl BundlePoller for S3BundlePoller {
       .client
       .get_object(get_object_request)
       .await
-      .context(S3GetObjectError)?;
+      .map_err(|source| InternalError::S3GetObjectError { source })?;
 
     let current_bundle_id = get_object_response
       .e_tag
       .ok_or(InternalError::S3MissingETag)?;
 
     if current_bundle_id != bundle_id {
-      return Err(Error::MismatchedBundleID {
+      return Err(PollerError::MismatchedBundleID {
         original_id: String::from(bundle_id),
         current_id: current_bundle_id,
       });
@@ -158,7 +164,7 @@ impl BundlePoller for S3BundlePoller {
     archive
       .unpack(path)
       .await
-      .map_err(|err| Error::UnpackError { source: err })?;
+      .map_err(|err| PollerError::UnpackError { source: err })?;
 
     Ok(())
   }
diff --git a/src/cli/bundle.rs b/src/cli/bundle.rs
index 0ede3af..0ec32e5 100644
--- a/src/cli/bundle.rs
+++ b/src/cli/bundle.rs
@@ -7,7 +7,7 @@ use espresso::{
 
 use super::args::BundleOpts;
 
-pub async fn bundle(config: Arc<Config>, opts: BundleOpts) -> Result<(), packager::Error> {
+pub async fn bundle(config: Arc<Config>, opts: BundleOpts) -> Result<(), packager::PackagerError> {
   let bundler = Bundler::new(config);
 
   bundler.package(opts.source_path).await.unwrap();
diff --git a/src/cli/serve.rs b/src/cli/serve.rs
index d494e6f..417cdd1 100644
--- a/src/cli/serve.rs
+++ b/src/cli/serve.rs
@@ -10,30 +10,47 @@ use espresso::{
   stats::{self, StatsServer},
 };
 use lazy_static::lazy_static;
-use snafu::{ResultExt, Snafu};
 use std::{
   collections::HashSet,
   sync::{mpsc, Arc},
 };
+use thiserror::Error;
 use tokio::sync::RwLock;
 
 lazy_static! {
   static ref MONITOR: ThreadMonitor = ThreadMonitor::new();
 }
 
-#[derive(Snafu, Debug)]
-pub enum Error {
-  Unbundle { source: bundle::Error },
-  ServeError { source: Box<server::Error> },
-  ServeStats { source: stats::Error },
-  MonitorError { source: monitor::Error },
+#[derive(Debug, Error)]
+pub enum ServeError {
+  #[error(transparent)]
+  Unbundle {
+    #[from]
+    source: bundle::BundleError,
+  },
+  #[error(transparent)]
+  ServeError {
+    #[from]
+    source: Box<server::ServerError>,
+  },
+  #[error(transparent)]
+  ServeStats {
+    #[from]
+    source: stats::StatsError,
+  },
+  #[error(transparent)]
+  MonitorError {
+    #[from]
+    source: monitor::Error,
+  },
+  #[error("Issue with receiving notification")]
   RecvNotify,
 }
 
-type Result<T, E = Error> = std::result::Result<T, E>;
+type Result<T, E = ServeError> = std::result::Result<T, E>;
 
 pub async fn serve(config: Arc<Config>) -> Result<()> {
-  MONITOR.init().context(MonitorError)?;
+  MONITOR.init()?;
 
   // Set up a channel for receiving thread notifications.
   let (monitor_tx, monitor_rx) = mpsc::channel();
@@ -52,7 +69,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> {
     server
       .spawn(monitor_tx.clone())
       .await
-      .map_err(|err| Error::ServeError {
+      .map_err(|err| ServeError::ServeError {
         source: Box::new(err),
       })?;
 
@@ -65,10 +82,8 @@ pub async fn serve(config: Arc<Config>) -> Result<()> {
     Some(stats_config) => {
       let stats_server = StatsServer::new(stats_config.clone(), unbundler.clone());
 
-      let (stats_server_handle, stats_thread_handle) = stats_server
-        .spawn(monitor_tx.clone())
-        .await
-        .context(ServeStats)?;
+      let (stats_server_handle, stats_thread_handle) =
+        stats_server.spawn(monitor_tx.clone()).await?;
 
       maybe_stats_server_handle = Some(stats_server_handle);
       server_thread_ids.insert(stats_thread_handle.thread().id());
@@ -79,9 +94,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> {
   let unbundler_thread_handle = thread::handle::spawn(monitor_tx.clone(), move || {
     let sys = System::new();
 
-    let result = sys
-      .block_on(async move { unbundler.enter().await })
-      .context(Unbundle);
+    let result = sys.block_on(async move { unbundler.enter().await });
 
     if let Err(e) = result {
       log::error!("Unbundler failed: {:?}", e);
@@ -105,7 +118,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> {
 
   // Wait for a thread to finish.
   loop {
-    monitor_rx.recv().map_err(|_| Error::RecvNotify)?;
+    monitor_rx.recv().map_err(|_| ServeError::RecvNotify)?;
 
     if Ok(true) == monitor_thread_handle.get_end_handle().has_ended() {
       log::info!("Stopping servers due to a panic.");
diff --git a/src/config.rs b/src/config.rs
index ef5f1fa..630c1b8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -2,36 +2,42 @@
 
 use crate::files::directory::{index::IndexStrategy, listing::default_listing_renderer};
 use serde_derive::{Deserialize, Serialize};
-use snafu::Snafu;
 use std::{
   collections::{HashMap, HashSet},
   net::SocketAddr,
   path::PathBuf,
   sync::Arc,
 };
+use thiserror::Error;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum ConfigError {
   /// The configuration file could not be found or read.
-  #[snafu(display("Could not open config from {}: {}", path.display(), source))]
+  #[error("Could not open config from {}: {}", path.display(), source)]
   OpenConfig {
     path: PathBuf,
+    #[source]
     source: std::io::Error,
   },
   /// The configuration file could not be parsed or deserialized.
-  #[snafu(display("Could not deserialize config from {}: {}", path.display(), source))]
+  #[error("Could not deserialize config from {}: {}", path.display(), source)]
   DeserializeConfig {
     path: PathBuf,
+    #[source]
     source: toml::de::Error,
   },
   /// The current directory could not be determined.
-  GetCurrentDir { source: std::io::Error },
+  #[error("Unable to get current directory")]
+  GetCurrentDir {
+    #[source]
+    source: std::io::Error,
+  },
   /// None of the provided paths were valid configuration files.
-  #[snafu(display("None of the provided paths were valid configuration files."))]
+  #[error("None of the provided paths were valid configuration files.")]
   NoValidPath,
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
+pub type Result<T, E = ConfigError> = std::result::Result<T, E>;
 
 /// Root configuration struct for Espresso servers.
 #[derive(Serialize, Deserialize, PartialEq, Debug)]
diff --git a/src/files/path.rs b/src/files/path.rs
index c5a8d20..0328037 100644
--- a/src/files/path.rs
+++ b/src/files/path.rs
@@ -1,18 +1,24 @@
-use snafu::{ResultExt, Snafu};
 use std::path::{Path, PathBuf};
+use thiserror::Error;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum PathError {
+  #[error(transparent)]
   StripPrefix {
+    #[from]
     source: std::path::StripPrefixError,
   },
+  #[error("Path has no parent")]
   NoParent,
+  #[error("Path is relative")]
   RelativePath,
-  #[snafu(display("Unable to canonicalize path (Path: {}, Source: {})", path.display(), source))]
+  #[error("Unable to canonicalize path (Path: {}, Source: {})", path.display(), source)]
   Canonicalize {
     path: PathBuf,
+    #[source]
     source: std::io::Error,
   },
+  #[error("Path is out of bounds")]
   PathOutOfBounds,
 }
 
@@ -24,19 +30,22 @@ pub fn resolve_path_within_tree(
   base_path: &Path,
   current_path: &Path,
   path: &Path,
-) -> Result<PathBuf, Error> {
+) -> Result<PathBuf, PathError> {
   let new_path = if path.is_absolute() {
-    base_path.join(path.strip_prefix("/").context(StripPrefix)?)
+    base_path.join(path.strip_prefix("/")?)
   } else if current_path.is_dir() {
     current_path.join(path)
   } else {
     current_path
       .parent()
       .map(|x| x.join(path))
-      .ok_or(Error::NoParent)?
+      .ok_or(PathError::NoParent)?
   };
 
-  new_path.canonicalize().context(Canonicalize {
-    path: new_path.clone(),
-  })
+  new_path
+    .canonicalize()
+    .map_err(|source| PathError::Canonicalize {
+      path: new_path.clone(),
+      source,
+    })
 }
diff --git a/src/files/path_context.rs b/src/files/path_context.rs
index 05c3c64..91b79c3 100644
--- a/src/files/path_context.rs
+++ b/src/files/path_context.rs
@@ -2,36 +2,42 @@ use super::directory::index::IndexStrategy;
 use crate::config::{ContentDispositionConfig, PathConfig, PathMatcherConfig, ServerConfig};
 use actix_web::http::header::{self, DispositionType, HeaderMap};
 use regex::Regex;
-use snafu::{ResultExt, Snafu};
 use std::{
   collections::HashMap,
   convert::{TryFrom, TryInto},
   fmt::Debug,
   path::{Path, PathBuf},
 };
-
-#[derive(Snafu, Debug)]
-pub enum Error {
-  #[snafu(display("Failed to parse the Mime type."))]
-  ParseMime { source: mime::FromStrError },
-  #[snafu(display("Failed to parse the header name: {}", name))]
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum PathContextError {
+  #[error("Failed to parse the Mime type.")]
+  ParseMime {
+    #[from]
+    source: mime::FromStrError,
+  },
+  #[error("Failed to parse the header name: {}", name)]
   ParseHeaderName {
+    #[source]
     source: header::InvalidHeaderName,
     name: String,
   },
-  #[snafu(display("Failed to parse the header value: {}", value))]
+  #[error("Failed to parse the header value: {}", value)]
   ParseHeaderValue {
+    #[source]
     source: header::InvalidHeaderValue,
     value: String,
   },
-  #[snafu(display("Failed to compile the Regex pattern: {}", pattern))]
+  #[error("Failed to compile the Regex pattern: {}", pattern)]
   InvalidRegexPattern {
     pattern: String,
+    #[source]
     source: regex::Error,
   },
 }
 
-type Result<T, E = Error> = std::result::Result<T, E>;
+type Result<T, E = PathContextError> = std::result::Result<T, E>;
 
 #[derive(Clone, Debug)]
 enum PathMatcher {
@@ -56,7 +62,7 @@ impl PathMatcher {
 }
 
 impl TryFrom<&PathMatcherConfig> for PathMatcher {
-  type Error = Error;
+  type Error = PathContextError;
 
   fn try_from(config: &PathMatcherConfig) -> Result<Self, Self::Error> {
     Ok(match config {
@@ -67,8 +73,9 @@ impl TryFrom<&PathMatcherConfig> for PathMatcher {
         prefix: prefix.clone(),
       },
       PathMatcherConfig::RegexMatcher { pattern } => PathMatcher::Regex {
-        regex: Regex::new(pattern).context(InvalidRegexPattern {
+        regex: Regex::new(pattern).map_err(|source| PathContextError::InvalidRegexPattern {
           pattern: pattern.clone(),
+          source,
         })?,
       },
     })
@@ -166,9 +173,9 @@ impl Debug for PathContext {
 }
 
 impl TryFrom<&PathConfig> for PathContext {
-  type Error = Error;
+  type Error = PathContextError;
 
-  fn try_from(config: &PathConfig) -> Result<Self, Error> {
+  fn try_from(config: &PathConfig) -> Result<Self, PathContextError> {
     let mime_disposition = try_get_mime_disposition(&config.mime_disposition)?;
     let headers = try_get_headers(&config.headers)?;
 
@@ -187,9 +194,9 @@ impl TryFrom<&PathConfig> for PathContext {
 }
 
 impl TryFrom<&ServerConfig> for PathContext {
-  type Error = Error;
+  type Error = PathContextError;
 
-  fn try_from(config: &ServerConfig) -> Result<Self, Error> {
+  fn try_from(config: &ServerConfig) -> Result<Self, PathContextError> {
     let mime_disposition = try_get_mime_disposition(&config.mime_disposition)?;
     let headers = try_get_headers(&config.headers)?;
 
@@ -213,7 +220,7 @@ fn try_get_mime_disposition(
 
     for (key, value) in mime_disposition.iter() {
       new_mime_disposition.insert(
-        key.clone().parse().context(ParseMime)?,
+        key.clone().parse()?,
         match value {
           ContentDispositionConfig::Inline => DispositionType::Inline,
           ContentDispositionConfig::Attachment => DispositionType::Attachment,
@@ -235,10 +242,16 @@ fn try_get_headers(headers: &Option<HashMap<String, String>>) -> Result<Option<H
       new_headers.insert(
         name
           .parse()
-          .context(ParseHeaderName { name: name.clone() })?,
-        value.parse().context(ParseHeaderValue {
-          value: value.clone(),
-        })?,
+          .map_err(|source| PathContextError::ParseHeaderName {
+            name: name.clone(),
+            source,
+          })?,
+        value
+          .parse()
+          .map_err(|source| PathContextError::ParseHeaderValue {
+            value: value.clone(),
+            source,
+          })?,
       );
     }
 
diff --git a/src/files/pathbuf.rs b/src/files/pathbuf.rs
index 429834e..11a324e 100644
--- a/src/files/pathbuf.rs
+++ b/src/files/pathbuf.rs
@@ -5,41 +5,41 @@ use actix_web::{
 };
 use futures_util::future::{ready, Ready};
 use percent_encoding::percent_decode_str;
-use snafu::Snafu;
 use std::{
   convert::{TryFrom, TryInto},
   ops::Deref,
   path::{Component, Path, PathBuf},
   str::FromStr,
 };
+use thiserror::Error;
 
-#[derive(Snafu, Debug, PartialEq)]
-pub enum Error {
+#[derive(Debug, Error, PartialEq)]
+pub enum PathBufError {
   /// The segment started with the wrapped invalid character.
-  #[snafu(display("The segment started with the wrapped invalid character."))]
+  #[error("The segment started with the wrapped invalid character: {}", char)]
   BadStart { char: char },
   /// The segment contained the wrapped invalid character.
-  #[snafu(display("The segment contained the wrapped invalid character."))]
+  #[error("The segment contained the wrapped invalid character: {}", char)]
   BadChar { char: char },
   /// The segment ended with the wrapped invalid character.
-  #[snafu(display("The segment ended with the wrapped invalid character."))]
+  #[error("The segment ended with the wrapped invalid character: {}", char)]
   BadEnd { char: char },
   /// The path is not a valid UTF-8 string after percent-decoding
-  #[snafu(display("The path is not a valid UTF-8 string after percent-decoding."))]
+  #[error("The path is not a valid UTF-8 string after percent-decoding.")]
   NotValidUtf8,
   /// The path has invalid or unexpected components.
-  #[snafu(display("The path has invalid or unexpected components."))]
+  #[error("The path has invalid or unexpected components.")]
   InvalidComponents,
 }
 
-/// Return `BadRequest` for `Error`.
-impl ResponseError for Error {
+/// Return `BadRequest` for `PathBufError`.
+impl ResponseError for PathBufError {
   fn status_code(&self) -> StatusCode {
     StatusCode::BAD_REQUEST
   }
 }
 
-type Result<T, E = Error> = std::result::Result<T, E>;
+type Result<T, E = PathBufError> = std::result::Result<T, E>;
 
 #[derive(Debug)]
 pub struct UriPathBuf(PathBuf);
@@ -52,10 +52,10 @@ impl UriPathBuf {
 
     let decoded_path = percent_decode_str(raw_path)
       .decode_utf8()
-      .map_err(|_| Error::NotValidUtf8)?;
+      .map_err(|_| PathBufError::NotValidUtf8)?;
 
     if segment_count != decoded_path.matches('/').count() + 1 {
-      return Err(Error::BadChar { char: '/' });
+      return Err(PathBufError::BadChar { char: '/' });
     }
 
     for segment in decoded_path.split('/') {
@@ -63,22 +63,22 @@ impl UriPathBuf {
         segment_count -= 1;
         path.pop();
       } else if segment.starts_with('.') {
-        return Err(Error::BadStart { char: '.' });
+        return Err(PathBufError::BadStart { char: '.' });
       } else if segment.starts_with('*') {
-        return Err(Error::BadStart { char: '*' });
+        return Err(PathBufError::BadStart { char: '*' });
       } else if segment.ends_with(':') {
-        return Err(Error::BadEnd { char: ':' });
+        return Err(PathBufError::BadEnd { char: ':' });
       } else if segment.ends_with('>') {
-        return Err(Error::BadEnd { char: '>' });
+        return Err(PathBufError::BadEnd { char: '>' });
       } else if segment.ends_with('<') {
-        return Err(Error::BadEnd { char: '<' });
+        return Err(PathBufError::BadEnd { char: '<' });
       } else if segment.is_empty() {
         segment_count -= 1;
         continue;
       } else if cfg!(windows) && segment.contains('\\') {
-        return Err(Error::BadChar { char: '\\' });
+        return Err(PathBufError::BadChar { char: '\\' });
       } else if cfg!(windows) && segment.contains(':') {
-        return Err(Error::BadChar { char: ':' });
+        return Err(PathBufError::BadChar { char: ':' });
       } else {
         path.push(segment)
       }
@@ -86,7 +86,7 @@ impl UriPathBuf {
 
     for (i, component) in path.components().enumerate() {
       if !matches!(component, Component::Normal(_)) || i >= segment_count {
-        return Err(Error::InvalidComponents);
+        return Err(PathBufError::InvalidComponents);
       }
     }
 
@@ -95,7 +95,7 @@ impl UriPathBuf {
 }
 
 impl FromStr for UriPathBuf {
-  type Err = Error;
+  type Err = PathBufError;
 
   fn from_str(raw_path: &str) -> Result<Self, Self::Err> {
     Self::new(raw_path)
@@ -103,7 +103,7 @@ impl FromStr for UriPathBuf {
 }
 
 impl FromRequest for UriPathBuf {
-  type Error = Error;
+  type Error = PathBufError;
   type Future = Ready<Result<Self, Self::Error>>;
 
   fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
@@ -112,17 +112,17 @@ impl FromRequest for UriPathBuf {
 }
 
 impl TryFrom<&HttpRequest> for UriPathBuf {
-  type Error = Error;
+  type Error = PathBufError;
 
-  fn try_from(request: &HttpRequest) -> Result<Self, Error> {
+  fn try_from(request: &HttpRequest) -> Result<Self, PathBufError> {
     request.match_info().unprocessed().parse()
   }
 }
 
 impl TryFrom<&ServiceRequest> for UriPathBuf {
-  type Error = Error;
+  type Error = PathBufError;
 
-  fn try_from(request: &ServiceRequest) -> Result<Self, Error> {
+  fn try_from(request: &ServiceRequest) -> Result<Self, PathBufError> {
     request.match_info().unprocessed().parse()
   }
 }
@@ -141,17 +141,17 @@ impl AsRef<Path> for UriPathBuf {
 
 #[cfg(test)]
 mod tests {
-  use super::{Error, UriPathBuf};
+  use super::{PathBufError, UriPathBuf};
   use std::{iter::FromIterator, path::PathBuf};
 
   #[actix_rt::test]
   async fn test_path_buf() {
-    let cases: &[(&str, Result<PathBuf, Error>)] = &[
-      ("/test/.tt", Err(Error::BadStart { char: '.' })),
-      ("/test/*tt", Err(Error::BadStart { char: '*' })),
-      ("/test/tt:", Err(Error::BadEnd { char: ':' })),
-      ("/test/tt<", Err(Error::BadEnd { char: '<' })),
-      ("/test/tt>", Err(Error::BadEnd { char: '>' })),
+    let cases: &[(&str, Result<PathBuf, PathBufError>)] = &[
+      ("/test/.tt", Err(PathBufError::BadStart { char: '.' })),
+      ("/test/*tt", Err(PathBufError::BadStart { char: '*' })),
+      ("/test/tt:", Err(PathBufError::BadEnd { char: ':' })),
+      ("/test/tt<", Err(PathBufError::BadEnd { char: '<' })),
+      ("/test/tt>", Err(PathBufError::BadEnd { char: '>' })),
       ("hello%20world", Ok(PathBuf::from_iter(vec!["hello world"]))),
       (
         "/testing%21/hello%20world",
@@ -165,7 +165,7 @@ mod tests {
       ),
       (
         "/../../../..%2F../dev/null",
-        Err(Error::BadChar { char: '/' }),
+        Err(PathBufError::BadChar { char: '/' }),
       ),
     ];
 
diff --git a/src/files/service.rs b/src/files/service.rs
index 33ae2d0..1599a6e 100644
--- a/src/files/service.rs
+++ b/src/files/service.rs
@@ -18,8 +18,6 @@ use actix_web::{
 };
 use async_recursion::async_recursion;
 use futures_util::future::LocalBoxFuture;
-use snafu::ResultExt;
-use snafu::Snafu;
 use std::{
   convert::TryInto,
   io,
@@ -29,60 +27,68 @@ use std::{
   sync::Arc,
   task::{Context, Poll},
 };
+use thiserror::Error;
 use tokio::sync::RwLock;
 
 /// Errors which can occur when serving static files.
-#[derive(Snafu, Debug)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum ServiceError {
   /// Path is not a directory
-  #[snafu(display("Path is not a directory. Unable to serve static files"))]
+  #[error("Path is not a directory. Unable to serve static files")]
   IsNotDirectory,
 
   /// Cannot render directory
-  #[snafu(display("Unable to render directory without index file"))]
+  #[error("Unable to render directory without index file")]
   IsDirectory,
 
-  #[snafu(display("Serve directory is not ready."))]
+  #[error("Serve directory is not ready.")]
   ServeDirNotReady,
 
-  #[snafu(display("Unable to obtain read lock for serve dir."))]
+  #[error("Unable to obtain read lock for serve dir.")]
   ServerDirReadLockFail,
 
-  #[snafu(display("Unable to find or open the specified file."))]
+  #[error("Unable to find or open the specified file.")]
   OpenFile {
+    #[source]
     source: io::Error,
   },
 
+  #[error("Unable to canonicalize path")]
   CanonicalizePath {
+    #[source]
     source: io::Error,
   },
 
-  MethodNotAllowed {
-    method: Method,
-  },
+  #[error("Method {} is not allowed", method)]
+  MethodNotAllowed { method: Method },
 
+  #[error(transparent)]
   BuildUriPath {
-    source: super::pathbuf::Error,
+    #[from]
+    source: super::pathbuf::PathBufError,
   },
 
-  RenderListing {
-    source: io::Error,
-  },
+  #[error("Unable to render listing")]
+  RenderListing { source: io::Error },
 
+  #[error("Unable to reconstruct request")]
   ReconstructRequest,
 
+  #[error("Unable to build file response")]
   BuildFileResponse {
+    #[source]
     source: ActixError,
   },
 
+  #[error("Custom path not found")]
   ServeCustomNotFoundPath,
 }
 
-/// Return `NotFound` for `Error`
-impl ResponseError for Error {
+/// Return `NotFound` for `ServiceError`
+impl ResponseError for ServiceError {
   fn error_response(&self) -> HttpResponse {
     match self {
-      Error::MethodNotAllowed { .. } => HttpResponse::MethodNotAllowed()
+      ServiceError::MethodNotAllowed { .. } => HttpResponse::MethodNotAllowed()
         .append_header((header::CONTENT_TYPE, "text/plain"))
         .body("Request did not meet this resource's requirements."),
       _ => HttpResponse::new(StatusCode::NOT_FOUND),
@@ -144,7 +150,7 @@ impl FilesServiceInner {
   #[async_recursion(?Send)]
   async fn handle_err(
     &self,
-    e: Error,
+    e: ServiceError,
     req: ServiceRequest,
     request_context: Option<RequestContext>,
   ) -> Result<ServiceResponse, ActixError> {
@@ -167,9 +173,9 @@ impl FilesServiceInner {
   async fn get_request_context_for_request(
     &self,
     req: &ServiceRequest,
-  ) -> Result<RequestContext, Error> {
+  ) -> Result<RequestContext, ServiceError> {
     let serve_dir = self.get_serve_dir().await?;
-    let path_from_request: UriPathBuf = req.try_into().context(BuildUriPath)?;
+    let path_from_request: UriPathBuf = req.try_into()?;
 
     Ok(RequestContext {
       path: serve_dir.join(&path_from_request),
@@ -180,7 +186,7 @@ impl FilesServiceInner {
 
   async fn handle_early_err(
     &self,
-    e: Error,
+    e: ServiceError,
     req: ServiceRequest,
   ) -> Result<ServiceResponse, ActixError> {
     let request_context = self.get_request_context_for_request(&req).await;
@@ -235,7 +241,7 @@ impl FilesServiceInner {
       ),
       Err(_) => {
         self
-          .handle_err(Error::ServeCustomNotFoundPath, req, None)
+          .handle_err(ServiceError::ServeCustomNotFoundPath, req, None)
           .await
       }
     }
@@ -277,7 +283,11 @@ impl FilesServiceInner {
             let req = ServiceRequest::from_parts(http_req, payload);
 
             self
-              .handle_err(Error::RenderListing { source }, req, Some(request_context))
+              .handle_err(
+                ServiceError::RenderListing { source },
+                req,
+                Some(request_context),
+              )
               .await
           }
         }
@@ -313,7 +323,11 @@ impl FilesServiceInner {
             let req = ServiceRequest::from_parts(http_req, payload);
 
             self
-              .handle_err(Error::RenderListing { source }, req, Some(request_context))
+              .handle_err(
+                ServiceError::RenderListing { source },
+                req,
+                Some(request_context),
+              )
               .await
           }
         }
@@ -337,12 +351,12 @@ impl FilesServiceInner {
         }
 
         self
-          .handle_err(Error::IsDirectory, req, Some(request_context))
+          .handle_err(ServiceError::IsDirectory, req, Some(request_context))
           .await
       }
       IndexStrategy::NoIndex => {
         self
-          .handle_err(Error::IsDirectory, req, Some(request_context))
+          .handle_err(ServiceError::IsDirectory, req, Some(request_context))
           .await
       }
     };
@@ -358,7 +372,7 @@ impl FilesServiceInner {
       path, path_context, ..
     } = &request_context;
 
-    match NamedFile::open(path).context(OpenFile) {
+    match NamedFile::open(path).map_err(|source| ServiceError::OpenFile { source }) {
       Ok(named_file) => {
         let (http_req, _payload) = req.into_parts();
 
@@ -386,28 +400,28 @@ impl FilesServiceInner {
     }
   }
 
-  async fn get_serve_dir(&self) -> Result<PathBuf, Error> {
+  async fn get_serve_dir(&self) -> Result<PathBuf, ServiceError> {
     let maybe_dir = self.directory.read().await.clone();
 
     match maybe_dir {
       Some(dir) => Ok(dir),
-      None => Err(Error::ServeDirNotReady),
+      None => Err(ServiceError::ServeDirNotReady),
     }
   }
 
   async fn get_canonical_path_from_request(
     &self,
     req: &ServiceRequest,
-  ) -> Result<(PathBuf, PathBuf, UriPathBuf), Error> {
+  ) -> Result<(PathBuf, PathBuf, UriPathBuf), ServiceError> {
     let serve_dir = self.get_serve_dir().await?;
 
-    let path_from_request: UriPathBuf = req.try_into().context(BuildUriPath)?;
+    let path_from_request: UriPathBuf = req.try_into()?;
 
     // Obtain the normalized full file path to use.
     let canonical_path = serve_dir
       .join(&path_from_request)
       .canonicalize()
-      .context(CanonicalizePath)?;
+      .map_err(|source| ServiceError::CanonicalizePath { source })?;
 
     Ok((canonical_path, serve_dir, path_from_request))
   }
@@ -456,7 +470,7 @@ impl Service<ServiceRequest> for FilesService {
       if !is_method_valid {
         return this
           .handle_early_err(
-            Error::MethodNotAllowed {
+            ServiceError::MethodNotAllowed {
               method: req.method().clone(),
             },
             req,
diff --git a/src/main.rs b/src/main.rs
index 100c803..10f604c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,19 +8,31 @@ use cli::{
 };
 use collective::cli::ConfigurableAppOpts;
 use espresso::bundle::packager;
-use snafu::{ResultExt, Snafu};
 use std::sync::Arc;
+use thiserror::Error;
 
 pub mod cli;
 
-#[derive(Snafu, Debug)]
-pub enum Error {
-  ServeError { source: serve::Error },
-  BundleError { source: packager::Error },
-  CliError { source: collective::cli::CliError },
+#[derive(Error, Debug)]
+pub enum MainError {
+  #[error(transparent)]
+  ServeError {
+    #[from]
+    source: serve::ServeError,
+  },
+  #[error(transparent)]
+  BundleError {
+    #[from]
+    source: packager::PackagerError,
+  },
+  #[error(transparent)]
+  CliError {
+    #[from]
+    source: collective::cli::CliError,
+  },
 }
 
-type Result<T, E = Error> = std::result::Result<T, E>;
+type Result<T, E = MainError> = std::result::Result<T, E>;
 
 #[actix_rt::main]
 async fn main() -> Result<()> {
@@ -29,7 +41,7 @@ async fn main() -> Result<()> {
 
   let result = inner_main().await;
 
-  if let Err(Error::CliError {
+  if let Err(MainError::CliError {
     source: collective::cli::CliError::ArgParse(inner),
   }) = &result
   {
@@ -40,12 +52,14 @@ async fn main() -> Result<()> {
 }
 
 async fn inner_main() -> Result<()> {
-  let (opts, config) = Opts::try_init_with_config().context(CliError)?;
+  let (opts, config) = Opts::try_init_with_config()?;
 
   let config = Arc::new(config);
 
   match opts.subcmd {
-    SubCommand::Serve => serve::serve(config).await.context(ServeError),
-    SubCommand::Bundle(opts) => bundle::bundle(config, opts).await.context(BundleError),
-  }
+    SubCommand::Serve => serve::serve(config).await?,
+    SubCommand::Bundle(opts) => bundle::bundle(config, opts).await?,
+  };
+
+  Ok(())
 }
diff --git a/src/server.rs b/src/server.rs
index a7f142e..177fcae 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -10,7 +10,6 @@ use actix_web::{
   App, HttpServer,
 };
 use collective::thread::{self, handle::ThreadHandle};
-use snafu::{ResultExt, Snafu};
 use std::{
   convert::{TryFrom, TryInto},
   path::PathBuf,
@@ -20,28 +19,39 @@ use std::{
   },
   thread::Thread,
 };
+use thiserror::Error;
 use tokio::sync::RwLock;
 
-#[derive(Debug, Snafu)]
-pub enum Error {
+#[derive(Debug, Error)]
+pub enum ServerError {
+  #[error(transparent)]
   ChannelReceive {
+    #[from]
     source: RecvError,
   },
+  #[error("Unable to bind")]
   Bind {
+    #[source]
     source: std::io::Error,
   },
+  #[error("Unable to start runtime")]
   SystemRun {
+    #[source]
     source: std::io::Error,
   },
+  #[error("Unable to parse redirect URI")]
   RedirectUrlParseError {
+    #[from]
     source: InvalidUri,
   },
+  #[error("Unable to create path context")]
   CreatePathContext {
-    source: crate::files::path_context::Error,
+    #[from]
+    source: crate::files::path_context::PathContextError,
   },
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
+pub type Result<T, E = ServerError> = std::result::Result<T, E>;
 
 pub struct Server {
   config: ServerConfig,
@@ -70,13 +80,12 @@ impl Server {
       Some(CompressionConfig::Auto) | None
     );
 
-    let root_path_context =
-      Arc::new(PathContext::try_from(&self.config).context(CreatePathContext)?);
+    let root_path_context = Arc::new(PathContext::try_from(&self.config)?);
     let mut path_contexts: Vec<PathContext> = Vec::new();
 
     if let Some(path_configs) = &self.config.path_configs {
       for path_config in path_configs {
-        path_contexts.push(path_config.try_into().context(CreatePathContext)?);
+        path_contexts.push(path_config.try_into()?);
       }
     }
 
@@ -106,17 +115,18 @@ impl Server {
           .default_service(files_service)
       })
       .bind(server_address)
-      .context(Bind)?
+      .map_err(|source| ServerError::Bind { source })?
       .shutdown_timeout(60)
       .run();
 
       let _ = tx.send(srv.handle());
 
-      rt.block_on(async { srv.await }).context(SystemRun)?;
+      rt.block_on(async { srv.await })
+        .map_err(|source| ServerError::SystemRun { source })?;
 
       Ok(())
     });
 
-    Ok((rx.recv().context(ChannelReceive)?, thread_handle))
+    Ok((rx.recv()?, thread_handle))
   }
 }
diff --git a/src/stats.rs b/src/stats.rs
index 0c513f2..3692414 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -9,22 +9,38 @@ use actix_web::{middleware::Logger, App, HttpResponse, HttpServer, Responder};
 use collective::thread::{self, handle::ThreadHandle};
 use mpsc::{RecvError, SendError, Sender};
 use serde::Serialize;
-use snafu::{ResultExt, Snafu};
 use std::{
   path::PathBuf,
   sync::{mpsc, Arc},
   thread::Thread,
 };
-
-#[derive(Debug, Snafu)]
-pub enum Error {
-  ChannelSend { source: SendError<ServerHandle> },
-  ChannelReceive { source: RecvError },
-  Bind { source: std::io::Error },
-  SystemRun { source: std::io::Error },
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum StatsError {
+  #[error(transparent)]
+  ChannelSend {
+    #[from]
+    source: SendError<ServerHandle>,
+  },
+  #[error(transparent)]
+  ChannelReceive {
+    #[from]
+    source: RecvError,
+  },
+  #[error("Unable to bind")]
+  Bind {
+    #[source]
+    source: std::io::Error,
+  },
+  #[error("Unable to start runtime")]
+  SystemRun {
+    #[source]
+    source: std::io::Error,
+  },
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
+pub type Result<T, E = StatsError> = std::result::Result<T, E>;
 
 struct State {
   unbundler: Arc<Unbundler>,
@@ -62,18 +78,19 @@ impl StatsServer {
           }))
       })
       .bind(server_address)
-      .context(Bind)?
+      .map_err(|source| StatsError::Bind { source })?
       .shutdown_timeout(60)
       .run();
 
-      tx.send(srv.handle()).context(ChannelSend)?;
+      tx.send(srv.handle())?;
 
-      rt.block_on(async { srv.await }).context(SystemRun)?;
+      rt.block_on(async { srv.await })
+        .map_err(|source| StatsError::SystemRun { source })?;
 
       Ok(())
     });
 
-    Ok((rx.recv().context(ChannelReceive)?, thread_handle))
+    Ok((rx.recv()?, thread_handle))
   }
 }
 
-- 
GitLab