Repository: mozilla/cargo-vet
Pull request #218 (2022-06-22 22:35:54)

Complete rework all of our error handling to favour explicit error ty…
…pes over printing and Report

This is a bit hacky because it's basically a first pass. After this we can refine the code more piecemeal.

Gankra: ![image](https://user-images.githubusercontent.com/1136864/175167229-901fe2e5-a944-4eaa-984a-6bed62e805bd.png)

Hmm hallucinating related...
Gankra: ![image](https://user-images.githubusercontent.com/1136864/175167486-dfb80fe2-935c-4a36-b25e-81a3bbe2b039.png)


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a9a8c72..2a30e63 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,30 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
 [[package]]
 name = "adler"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -34,6 +52,21 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "backtrace"
+version = "0.3.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
 [[package]]
 name = "base64"
 version = "0.13.0"
@@ -86,7 +119,6 @@ dependencies = [
  "console",
  "crates-index",
  "dirs",
- "eyre",
  "filetime",
  "flate2",
  "futures-util",
@@ -94,6 +126,7 @@ dependencies = [
  "insta",
  "lazy_static",
  "libc",
+ "miette",
  "nom",
  "reqwest",
  "serde",
@@ -101,10 +134,12 @@ dependencies = [
  "tar",
  "tempfile",
  "textwrap",
+ "thiserror",
  "tokio",
  "toml_edit",
  "tracing",
  "tracing-subscriber",
+ "url",
  "winapi",
 ]
 
@@ -285,16 +320,6 @@ dependencies = [
  "cfg-if",
 ]
 
-[[package]]
-name = "eyre"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
-dependencies = [
- "indenter",
- "once_cell",
-]
-
 [[package]]
 name = "fastrand"
 version = "1.7.0"
@@ -394,6 +419,12 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "gimli"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
+
 [[package]]
 name = "git2"
 version = "0.14.4"
@@ -547,12 +578,6 @@ dependencies = [
  "unicode-normalization",
 ]
 
-[[package]]
-name = "indenter"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
-
 [[package]]
 name = "indexmap"
 version = "1.8.2"
@@ -592,6 +617,12 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
 
+[[package]]
+name = "is_ci"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
+
 [[package]]
 name = "itertools"
 version = "0.10.3"
@@ -688,6 +719,37 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "miette"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c"
+dependencies = [
+ "atty",
+ "backtrace",
+ "miette-derive",
+ "once_cell",
+ "owo-colors",
+ "supports-color",
+ "supports-hyperlinks",
+ "supports-unicode",
+ "terminal_size",
+ "textwrap",
+ "thiserror",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.16"
@@ -741,6 +803,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "object"
+version = "0.28.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.12.0"
@@ -753,6 +824,12 @@ version = "6.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
 
+[[package]]
+name = "owo-colors"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
+
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -845,6 +922,8 @@ version = "1.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
 dependencies = [
+ "aho-corasick",
+ "memchr",
  "regex-syntax",
 ]
 
@@ -917,6 +996,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -1072,6 +1157,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "smawk"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+
 [[package]]
 name = "socket2"
 version = "0.4.4"
@@ -1100,6 +1191,34 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "supports-color"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9"
+dependencies = [
+ "atty",
+ "is_ci",
+]
+
+[[package]]
+name = "supports-hyperlinks"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406"
+dependencies = [
+ "atty",
+]
+
+[[package]]
+name = "supports-unicode"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2"
+dependencies = [
+ "atty",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.96"
@@ -1159,6 +1278,11 @@ name = "textwrap"
 version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
 
 [[package]]
 name = "thiserror"
@@ -1354,6 +1478,15 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
 
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
+dependencies = [
+ "regex",
+]
+
 [[package]]
 name = "unicode-normalization"
 version = "0.1.19"
diff --git a/Cargo.toml b/Cargo.toml
index 94e416a..6a047fa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,6 @@ clap-cargo = "0.8.0"
 console = "0.15.0"
 crates-index = { version = "0.18.8", default-features = false }
 dirs = "4.0.0"
-eyre = "0.6.8"
 filetime = "0.2.16"
 flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
 futures-util = { version = "0.3.21", default-features = false, features = ["std"] }
@@ -40,6 +39,9 @@ toml_edit = { version = "0.14.4", features = ["serde"] }
 tokio = { version = "1.12.0", features = ["fs", "macros", "process", "rt-multi-thread"] }
 tracing = { version = "0.1.34", features = ["log"] }
 tracing-subscriber = "0.3.11"
+miette = { version = "4.7.1", features = ["fancy"] }
+thiserror = "1.0.31"
+url = "2.2.2"
 
 [target.'cfg(windows)'.dependencies.winapi]
 version = "0.3"
diff --git a/src/editor.rs b/src/editor.rs
index 09806f1..543deea 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -9,7 +9,7 @@ use std::str;
 use tempfile::NamedTempFile;
 use tracing::warn;
 
-use crate::VetError;
+use crate::errors::EditError;
 
 #[cfg(windows)]
 fn git_sh_path() -> Option<PathBuf> {
@@ -199,18 +199,18 @@ impl<'a> Editor<'a> {
 
     /// Run the editor, collecting and filtering the resulting file, and
     /// returning it as a string.
-    pub fn edit(self) -> Result<String, VetError> {
+    pub fn edit(self) -> Result<String, EditError> {
         // Close our handle on the file to allow other programs like the editor
         // to modify it on Windows.
         let path = self.tempfile.into_temp_path();
-        (self.run_editor)(&path)?;
+        (self.run_editor)(&path).map_err(EditError::CouldntLaunch)?;
 
         // Read in the result, filtering lines, and restoring unix line endings.
         // This is roughly based on git's logic for cleaning up commit message
         // files.
         let mut lines: Vec<String> = Vec::new();
-        for line in BufReader::new(File::open(&path)?).lines() {
-            let line = line?;
+        for line in BufReader::new(File::open(&path).map_err(EditError::CouldntOpen)?).lines() {
+            let line = line.map_err(EditError::CouldntRead)?;
             // Ignore lines starting with a comment character.
             if line.starts_with(self.comment_char) {
                 continue;
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..933e530
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,582 @@
+use std::{ffi::OsString, fmt::Display, path::PathBuf, string::FromUtf8Error, sync::Arc};
+
+use cargo_metadata::Version;
+use miette::{Diagnostic, NamedSource};
+use thiserror::Error;
+
+use crate::format::{ImportName, PackageName};
+
+pub type SourceFile = Arc<NamedSource>;
+
+///////////////////////////////////////////////////////////
+// AuditAsErrors
+///////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("There are some issues with your policy.audit-as-crates-io entries")]
+#[diagnostic()]
+pub struct AuditAsErrors {
+    #[related]
+    pub errors: Vec<AuditAsError>,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum AuditAsError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    NeedsAuditAs(NeedsAuditAsErrors),
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    ShouldntBeAuditAs(ShouldntBeAuditAsErrors),
+    // FIXME: we should probably just make the caller pass this in?
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    CacheAcquire(CacheAcquireError),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[diagnostic(help("Add a `policy.*.audit-as-crates-io` entry for them"))]
+pub struct NeedsAuditAsErrors {
+    pub errors: Vec<NeedsAuditAsError>,
+}
+
+impl Display for NeedsAuditAsErrors {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("Some first-party packages match published crates.io versions")?;
+        for e in &self.errors {
+            f.write_fmt(format_args!("\n  {}", e))?
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[diagnostic(help("Remove the audit-as-crates-io entries or make them `false`"))]
+pub struct ShouldntBeAuditAsErrors {
+    pub errors: Vec<ShouldntBeAuditAsError>,
+}
+
+impl Display for ShouldntBeAuditAsErrors {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("some audit-as-crates-io packages don't match published crates.io versions")?;
+        for e in &self.errors {
+            f.write_fmt(format_args!("\n  {}", e))?
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Error)]
+#[error("{package}:{version}")]
+pub struct NeedsAuditAsError {
+    pub package: PackageName,
+    pub version: Version,
+}
+
+#[derive(Debug, Error)]
+#[error("{package}:{version}")]
+pub struct ShouldntBeAuditAsError {
+    pub package: PackageName,
+    pub version: Version,
+}
+
+///////////////////////////////////////////////////////////
+// CertifyError
+///////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum CertifyError {
+    #[error("no criteria chosen, aborting")]
+    NoCriteriaChosen,
+    #[error("couldn't guess what version of {0} to certify, please specify")]
+    CouldntGuessVersion(PackageName),
+    #[error("couldn't guess what package to certify, please specify")]
+    CouldntGuessPackage,
+    #[error("couldn't find uncommented certify statement")]
+    CouldntFindCertifyStatement,
+    #[error("'{0}' isn't one of your foreign packages")]
+    NotAPackage(PackageName),
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+    #[error(transparent)]
+    EditError(#[from] EditError),
+    #[error(transparent)]
+    UserInfoError(#[from] UserInfoError),
+}
+
+///////////////////////////////////////////////////////////
+// EditError
+///////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum EditError {
+    #[error("Failed to launch editor")]
+    CouldntLaunch(#[diagnostic(cause)] std::io::Error),
+    #[error("Failed to open result of editor")]
+    CouldntOpen(#[diagnostic(cause)] std::io::Error),
+    #[error("Failed to read result of editor")]
+    CouldntRead(#[diagnostic(cause)] std::io::Error),
+}
+
+///////////////////////////////////////////////////////////
+// InitErrors
+///////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Failed to initialize the cargo-vet store (supply-chain)")]
+#[non_exhaustive]
+pub enum InitError {
+    StoreCreate(#[diagnostic(cause)] StoreCreateError),
+    StoreCommit(#[diagnostic(cause)] StoreCommitError),
+}
+
+//////////////////////////////////////////////////////////
+// MinimizeUnauditedError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum MinimizeUnauditedError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Suggest(#[from] SuggestError),
+    #[error("An unknown error occured while trying to minimize unaudited entries")]
+    Unknown,
+}
+
+///////////////////////////////////////////////////////////
+// StoreErrors
+///////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+#[error("Couldn't create the store (supply-chain)")]
+pub enum StoreCreateError {
+    CouldntCreate(#[diagnostic(cause)] std::io::Error),
+    CouldntAcquire(
+        #[from]
+        #[diagnostic(cause)]
+        FlockError,
+    ),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreAcquireError {
+    #[error("Couldn't acquire the store's (supply-chain's) lock")]
+    CouldntLock(
+        #[from]
+        #[diagnostic(cause)]
+        FlockError,
+    ),
+    #[error(transparent)]
+    LoadToml(
+        #[from]
+        #[diagnostic(cause)]
+        LoadTomlError,
+    ),
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Validate(#[from] StoreValidateErrors),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Your cargo-vet store (supply-chain) has consistency errors")]
+pub struct StoreValidateErrors {
+    #[related]
+    errors: Vec<StoreValidateError>,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreValidateError {}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+#[error("Failed to commit store")]
+pub enum StoreCommitError {
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+    StoreToml(
+        #[from]
+        #[diagnostic(cause)]
+        StoreTomlError,
+    ),
+}
+
+//////////////////////////////////////////////////////////
+// CacheErrors
+/////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum CacheAcquireError {
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+    #[error("Failed to create cache root dir: {}", target.display())]
+    Root {
+        target: PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Failed to create cache src dir: {}", target.display())]
+    Src {
+        target: PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Failed to create cache empty dir: {}", target.display())]
+    Empty {
+        target: PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Failed to create cache package dir: {}", target.display())]
+    Cache {
+        target: PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Couldn't acquire the cache's lock")]
+    CouldntLock(
+        #[from]
+        #[diagnostic(cause)]
+        FlockError,
+    ),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+#[error("Failed to commit cache")]
+pub enum CacheCommitError {
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+    StoreToml(
+        #[from]
+        #[diagnostic(cause)]
+        StoreTomlError,
+    ),
+    StoreJson(
+        #[from]
+        #[diagnostic(cause)]
+        StoreJsonError,
+    ),
+}
+
+//////////////////////////////////////////////////////////
+/// CommandError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum CommandError {
+    #[error("Command failed")]
+    CommandFailed(#[diagnostic(cause)] std::io::Error),
+    #[error("Bad status {0}")]
+    BadStatus(i32),
+    #[error("Wasn't UTF-8")]
+    BadOutput(#[diagnostic(cause)] FromUtf8Error),
+}
+
+//////////////////////////////////////////////////////////
+// FetchAndDiffError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum FetchAndDiffError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Diff(#[from] DiffError),
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Fetch(#[from] FetchError),
+}
+
+//////////////////////////////////////////////////////////
+// DiffError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum DiffError {
+    #[error("Failed to diff package")]
+    CommandError(
+        #[from]
+        #[diagnostic(cause)]
+        CommandError,
+    ),
+}
+
+//////////////////////////////////////////////////////////
+// UserInfoError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum UserInfoError {
+    #[error("Failed to get user.name")]
+    UserCommandFailed(#[diagnostic(cause)] CommandError),
+    #[error("Failed to get user.email")]
+    EmailCommandFailed(#[diagnostic(cause)] CommandError),
+}
+
+//////////////////////////////////////////////////////////
+// FetchError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum FetchError {
+    #[error("Invalid URL for package: {url}")]
+    InvalidUrl {
+        url: String,
+        #[diagnostic(cause)]
+        error: url::ParseError,
+    },
+    #[error("Running as --frozen but needed to fetch {package}:{version}")]
+    Frozen {
+        package: PackageName,
+        version: Version,
+    },
+    #[error("Failed to unpack .crate at {}", src.display())]
+    Unpack {
+        src: PathBuf,
+        #[diagnostic(cause)]
+        error: UnpackError,
+    },
+    #[error("failed to open cached .crate at {}", target.display())]
+    OpenCached {
+        target: std::path::PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Download(#[from] DownloadError),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum UnpackError {
+    #[error("Failed to iterate archive")]
+    ArchiveIterate(#[diagnostic(cause)] std::io::Error),
+    #[error("Failed to read archive entry")]
+    ArchiveEntry(#[diagnostic(cause)] std::io::Error),
+    #[error("Invalid archive, {} wasn't under {}", entry_path.display(), prefix.to_string_lossy())]
+    InvalidPaths {
+        entry_path: PathBuf,
+        prefix: OsString,
+    },
+    #[error("Failed to unpack archive entry {}", entry_path.display())]
+    Unpack {
+        entry_path: PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Failed to finalize unpack to {}", target.display())]
+    LockCreate {
+        target: std::path::PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+}
+
+//////////////////////////////////////////////////////////
+// FetchAuditError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum FetchAuditError {
+    // FIXME: would have to explicitly import URL for this error
+    #[error("invalid URL for foreign import {import_name} @ {import_url}")]
+    InvalidUrl {
+        import_name: ImportName,
+        import_url: String,
+        #[diagnostic(cause)]
+        error: url::ParseError,
+    },
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    Download(#[from] DownloadError),
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    Toml(#[from] TomlParseError),
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    Validate(#[from] StoreValidateErrors),
+}
+
+//////////////////////////////////////////////////////////
+// DownloadError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum DownloadError {
+    #[error("failed to start download of {url}")]
+    FailedToStartDownload {
+        url: reqwest::Url,
+        #[diagnostic(cause)]
+        error: reqwest::Error,
+    },
+    #[error("failed to create file for download to {}", target.display())]
+    FailedToCreateDownload {
+        target: std::path::PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("failed to read download from {url}")]
+    FailedToReadDownload {
+        url: reqwest::Url,
+        #[diagnostic(cause)]
+        error: reqwest::Error,
+    },
+    #[error("failed to write download to {}", target.display())]
+    FailedToWriteDownload {
+        target: std::path::PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("failed to rename download to final location {}", target.display())]
+    FailedToFinalizeDownload {
+        target: std::path::PathBuf,
+        #[diagnostic(cause)]
+        error: std::io::Error,
+    },
+    #[error("Download wasn't valid utf8: {url}")]
+    InvalidText {
+        url: reqwest::Url,
+        #[diagnostic(cause)]
+        error: FromUtf8Error,
+    },
+}
+
+//////////////////////////////////////////////////////////
+// SuggestError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum SuggestError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    CacheAcquire(#[from] CacheAcquireError),
+}
+
+//////////////////////////////////////////////////////////
+// FlockError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum FlockError {
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+}
+
+//////////////////////////////////////////////////////////
+// TomlError/JsonError
+//////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Failed to parse toml file")]
+pub struct TomlParseError {
+    // #[source_code]
+    // input: SourceFile,
+    // #[label("here")]
+    // span: SourceOffset,
+    #[diagnostic(cause)]
+    pub error: toml_edit::de::Error,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Failed to parse json file")]
+pub struct JsonParseError {
+    // #[source_code]
+    // input: SourceFile,
+    // #[label("here")]
+    // span: SourceOffset,
+    #[diagnostic(cause)]
+    pub error: serde_json::Error,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum LoadTomlError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    TomlParse(#[from] TomlParseError),
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum LoadJsonError {
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    JsonParse(#[from] JsonParseError),
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreJsonError {
+    #[error(transparent)]
+    JsonSerialize(#[from] serde_json::Error),
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreTomlError {
+    #[error(transparent)]
+    TomlSerialize(#[from] toml_edit::ser::Error),
+    #[error(transparent)]
+    IoError(
+        #[from]
+        #[diagnostic(cause)]
+        std::io::Error,
+    ),
+}
diff --git a/src/flock.rs b/src/flock.rs
index c82920e..272e17a 100644
--- a/src/flock.rs
+++ b/src/flock.rs
@@ -10,11 +10,10 @@ use std::io;
 use std::io::{Read, Seek, SeekFrom, Write};
 use std::path::{Display, Path, PathBuf};
 
-use crate::VetError;
-use eyre::WrapErr;
-
 use sys::*;
 
+use crate::errors::FlockError;
+
 #[derive(Debug)]
 pub struct FileLock {
     f: Option<File>,
@@ -130,9 +129,8 @@ impl Filesystem {
     ///
     /// Handles errors where other processes are also attempting to concurrently
     /// create this directory.
-    pub fn create_dir(&self) -> Result<(), VetError> {
+    pub fn create_dir(&self) -> Result<(), std::io::Error> {
         fs::create_dir_all(&self.root)
-            .wrap_err_with(|| format!("failed to create directory `{}`", self.display()))
     }
 
     /// Returns an adaptor that can be used to print the path of this
@@ -151,7 +149,7 @@ impl Filesystem {
     ///
     /// The returned file can be accessed to look at the path and also has
     /// read/write access to the underlying file.
-    pub fn open_rw<P>(&self, path: P, msg: &str) -> Result<FileLock, VetError>
+    pub fn open_rw<P>(&self, path: P, msg: &str) -> Result<FileLock, FlockError>
     where
         P: AsRef<Path>,
     {
@@ -172,7 +170,7 @@ impl Filesystem {
     /// The returned file can be accessed to look at the path and also has read
     /// access to the underlying file. Any writes to the file will return an
     /// error.
-    pub fn open_ro<P>(&self, path: P, msg: &str) -> Result<FileLock, VetError>
+    pub fn open_ro<P>(&self, path: P, msg: &str) -> Result<FileLock, FlockError>
     where
         P: AsRef<Path>,
     {
@@ -190,26 +188,21 @@ impl Filesystem {
         opts: &OpenOptions,
         state: State,
         msg: &str,
-    ) -> Result<FileLock, VetError> {
+    ) -> Result<FileLock, FlockError> {
         let path = self.root.join(path);
 
         // If we want an exclusive lock then if we fail because of NotFound it's
         // likely because an intermediate directory didn't exist, so try to
         // create the directory and then continue.
-        let f = opts
-            .open(&path)
-            .or_else(|e| {
-                if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
-                    let parent = path.parent().unwrap();
-                    fs::create_dir_all(parent).wrap_err_with(|| {
-                        format!("failed to create directory `{}`", parent.display())
-                    })?;
-                    Ok(opts.open(&path)?)
-                } else {
-                    Err(VetError::from(e))
-                }
-            })
-            .wrap_err_with(|| format!("failed to open: {}", path.display()))?;
+        let f = opts.open(&path).or_else(|e| {
+            if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
+                let parent = path.parent().unwrap();
+                fs::create_dir_all(parent)?;
+                Ok(opts.open(&path)?)
+            } else {
+                Err(e)
+            }
+        })?;
         match state {
             State::Exclusive => {
                 acquire(msg, &path, &|| try_lock_exclusive(&f), &|| {
@@ -256,7 +249,7 @@ fn acquire(
     path: &Path,
     lock_try: &dyn Fn() -> io::Result<()>,
     lock_block: &dyn Fn() -> io::Result<()>,
-) -> Result<(), VetError> {
+) -> Result<(), FlockError> {
     // File locking on Unix is currently implemented via `flock`, which is known
     // to be broken on NFS. We could in theory just ignore errors that happen on
     // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
@@ -281,13 +274,13 @@ fn acquire(
 
         Err(e) => {
             if !error_contended(&e) {
-                return Err(e).wrap_err(format!("failed to lock file: {}", path.display()));
+                Err(e)?;
             }
         }
     }
     eprintln!("Blocking: waiting for file lock on {}", msg);
 
-    lock_block().wrap_err_with(|| format!("failed to lock file: {}", path.display()))?;
+    lock_block()?;
     return Ok(());
 
     #[cfg(all(target_os = "linux", not(target_env = "musl")))]
diff --git a/src/main.rs b/src/main.rs
index 3f9b0c6..d468bc0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,15 +9,21 @@ use std::{fs::File, panic, path::PathBuf};
 use cargo_metadata::{Metadata, Package};
 use clap::{CommandFactory, Parser};
 use console::Term;
-use eyre::{eyre, WrapErr};
+use errors::{
+    AuditAsError, AuditAsErrors, CertifyError, DiffError, MinimizeUnauditedError,
+    NeedsAuditAsError, NeedsAuditAsErrors, ShouldntBeAuditAsError, ShouldntBeAuditAsErrors,
+    UserInfoError,
+};
 use format::{CriteriaName, CriteriaStr, PackageName, PolicyEntry};
 use futures_util::future::join_all;
+use miette::{miette, Context, IntoDiagnostic};
 use network::Network;
 use reqwest::Url;
 use serde::de::Deserialize;
 use tracing::{error, info, trace};
 
 use crate::cli::*;
+use crate::errors::{CommandError, DownloadError};
 use crate::format::{
     AuditEntry, AuditKind, AuditsFile, ConfigFile, CriteriaEntry, Delta, DependencyCriteria,
     FetchCommand, ImportsFile, MetaConfig, MetaConfigInstance, PackageStr, SortedMap, StoreInfo,
@@ -29,6 +35,7 @@ use crate::storage::{Cache, Store};
 
 mod cli;
 mod editor;
+pub mod errors;
 mod flock;
 pub mod format;
 pub mod network;
@@ -39,8 +46,6 @@ pub mod storage;
 #[cfg(test)]
 mod tests;
 
-pub type VetError = eyre::Report;
-
 /// Absolutely All The Global Configurations
 pub struct Config {
     /// Cargo.toml `metadata.vet`
@@ -107,7 +112,7 @@ struct ExitPanic(i32);
 /// as our final act.
 struct ExecPanic(std::process::Command);
 
-fn main() -> Result<(), VetError> {
+fn main() -> Result<(), ()> {
     // NOTE: Limit the maximum number of blocking threads to 128, rather than
     // the default of 512.
     // This may limit concurrency in some cases, but cargo-vet isn't running a
@@ -122,8 +127,8 @@ fn main() -> Result<(), VetError> {
 
     // Wrap main up in a catch_panic so that we can use it to implement std::process::exit with
     // unwinding, allowing us to silently exit the program while still cleaning up.
-    let result = std::panic::catch_unwind(real_main);
-    match result {
+    let panic_result = std::panic::catch_unwind(real_main);
+    let main_result = match panic_result {
         Ok(main_result) => main_result,
         Err(mut e) => {
             if let Some(ExitPanic(code)) = e.downcast_ref::<ExitPanic>() {
@@ -142,10 +147,14 @@ fn main() -> Result<(), VetError> {
                 std::panic::resume_unwind(e);
             }
         }
-    }
+    };
+    main_result.map_err(|e| {
+        eprintln!("{:?}", e);
+        std::process::exit(-1);
+    })
 }
 
-fn real_main() -> Result<(), VetError> {
+fn real_main() -> Result<(), miette::Report> {
     use cli::Commands::*;
 
     let fake_cli = cli::FakeCli::parse();
@@ -202,6 +211,22 @@ fn real_main() -> Result<(), VetError> {
         );
     }));
 
+    // FIXME: we should have separate configs for errors but this works for now
+    let error_colors_enabled = cli.output_file.is_none();
+    miette::set_hook(Box::new(move |_| {
+        let graphical_theme = if error_colors_enabled {
+            miette::GraphicalTheme::unicode()
+        } else {
+            miette::GraphicalTheme::unicode_nocolor()
+        };
+        Box::new(
+            miette::MietteHandlerOpts::new()
+                .graphical_theme(graphical_theme)
+                .build(),
+        )
+    }))
+    .expect("Failed to initialize error handler");
+
     // Setup our output stream
     let mut stdout;
     let mut output_f;
@@ -287,6 +312,7 @@ fn real_main() -> Result<(), VetError> {
     // ERRORS: immediate fatal diagnostic
     let metadata = cmd
         .exec()
+        .into_diagnostic()
         .wrap_err("'cargo metadata' exited unsuccessfully")?;
 
     // trace!("Got Metadata! {:#?}", metadata);
@@ -315,6 +341,7 @@ fn real_main() -> Result<(), VetError> {
         .map(|cfg| {
             // ERRORS: immediate fatal diagnostic
             MetaConfigInstance::deserialize(cfg)
+                .into_diagnostic()
                 .wrap_err("Workspace had [{WORKSPACE_VET_CONFIG}] but it was malformed")
         })
         .transpose()?;
@@ -326,13 +353,14 @@ fn real_main() -> Result<(), VetError> {
         .map(|cfg| {
             // ERRORS: immediate fatal diagnostic
             MetaConfigInstance::deserialize(cfg)
+                .into_diagnostic()
                 .wrap_err("Root package had [{PACKAGE_VET_CONFIG}] but it was malformed")
         })
         .transpose()?;
 
     if workspace_metacfg.is_some() && package_metacfg.is_some() {
         // ERRORS: immediate fatal diagnostic
-        return Err(eyre!("Both a workspace and a package defined [metadata.vet]! We don't know what that means, if you do, let us know!"));
+        return Err(miette!("Both a workspace and a package defined [metadata.vet]! We don't know what that means, if you do, let us know!"));
     }
 
     let mut metacfgs = vec![default_config];
@@ -356,14 +384,14 @@ fn real_main() -> Result<(), VetError> {
     if matches!(cli.command, Some(Commands::Init { .. })) {
         if init {
             // ERRORS: immediate fatal diagnostic
-            return Err(eyre!(
+            return Err(miette!(
                 "'cargo vet' already initialized (store found at {})",
                 metacfg.store_path().display()
             ));
         }
     } else if !init {
         // ERRORS: immediate fatal diagnostic
-        return Err(eyre!(
+        return Err(miette!(
             "You must run 'cargo vet init' (store not found at {})",
             metacfg.store_path().display()
         ));
@@ -392,13 +420,13 @@ fn real_main() -> Result<(), VetError> {
     }
 }
 
-fn cmd_init(_out: &mut dyn Out, cfg: &Config, _sub_args: &InitArgs) -> Result<(), VetError> {
+fn cmd_init(_out: &mut dyn Out, cfg: &Config, _sub_args: &InitArgs) -> Result<(), miette::Report> {
     // Initialize vet
     trace!("initializing...");
 
     let mut store = Store::create(cfg)?;
 
-    let (config, audits, imports) = init_files(&cfg.metadata, cfg.cli.filter_graph.as_ref())?;
+    let (config, audits, imports) = init_files(&cfg.metadata, cfg.cli.filter_graph.as_ref());
     store.config = config;
     store.audits = audits;
     store.imports = imports;
@@ -411,7 +439,7 @@ fn cmd_init(_out: &mut dyn Out, cfg: &Config, _sub_args: &InitArgs) -> Result<()
 pub fn init_files(
     metadata: &Metadata,
     filter_graph: Option<&Vec<GraphFilter>>,
-) -> Result<(ConfigFile, AuditsFile, ImportsFile), VetError> {
+) -> (ConfigFile, AuditsFile, ImportsFile) {
     // Default audits file is empty
     let audits = AuditsFile {
         criteria: SortedMap::new(),
@@ -458,14 +486,14 @@ pub fn init_files(
         }
     };
 
-    Ok((config, audits, imports))
+    (config, audits, imports)
 }
 
 fn cmd_inspect(
     out: &mut dyn Out,
     cfg: &PartialConfig,
     sub_args: &InspectArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Download a crate's source to a temp location for review
     let cache = Cache::acquire(cfg)?;
     let network = Network::acquire(cfg);
@@ -477,18 +505,16 @@ fn cmd_inspect(
 
     let package = &*sub_args.package;
 
-    let fetched = tokio::runtime::Handle::current().block_on(cache.fetch_package(
-        network.as_ref(),
-        package,
-        &sub_args.version,
-    ))?;
+    let fetched = tokio::runtime::Handle::current()
+        .block_on(cache.fetch_package(network.as_ref(), package, &sub_args.version))
+        .into_diagnostic()?;
 
     #[cfg(target_family = "unix")]
     {
         // Loosely borrowed from cargo crev.
         let shell = std::env::var_os("SHELL").unwrap();
-        writeln!(out, "Opening nested shell in: {:#?}", fetched)?;
-        writeln!(out, "Use `exit` or Ctrl-D to finish.",)?;
+        writeln!(out, "Opening nested shell in: {:#?}", fetched).into_diagnostic()?;
+        writeln!(out, "Use `exit` or Ctrl-D to finish.",).into_diagnostic()?;
         let mut command = std::process::Command::new(shell);
         command.current_dir(fetched.clone()).env("PWD", fetched);
         panic_any(ExecPanic(command));
@@ -496,12 +522,16 @@ fn cmd_inspect(
 
     #[cfg(not(target_family = "unix"))]
     {
-        writeln!(out, "  fetched to {:#?}", fetched)?;
+        writeln!(out, "  fetched to {:#?}", fetched).into_diagnostic()?;
         Ok(())
     }
 }
 
-fn cmd_certify(out: &mut dyn Out, cfg: &Config, sub_args: &CertifyArgs) -> Result<(), VetError> {
+fn cmd_certify(
+    out: &mut dyn Out,
+    cfg: &Config,
+    sub_args: &CertifyArgs,
+) -> Result<(), miette::Report> {
     // Certify that you have reviewed a crate's source for some version / delta
     let mut store = Store::acquire(cfg)?;
     let network = Network::acquire(cfg);
@@ -522,7 +552,7 @@ fn do_cmd_certify(
     store: &mut Store,
     network: Option<&Network>,
     last_fetch: Option<FetchCommand>,
-) -> Result<(), VetError> {
+) -> Result<(), CertifyError> {
     // Before setting up magic, we need to agree on a package
     let package = if let Some(package) = &sub_args.package {
         package.clone()
@@ -530,26 +560,12 @@ fn do_cmd_certify(
         // If we just fetched a package, assume we want to certify it
         last_fetch.package().to_owned()
     } else {
-        // ERRORS: immediate fatal diagnostic
-        writeln!(
-            out,
-            "error: couldn't guess what package to certify, please specify"
-        )?;
-        panic_any(ExitPanic(-1));
+        return Err(CertifyError::CouldntGuessPackage);
     };
 
     // FIXME: can/should we check if the version makes sense..?
     if !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == *package) {
-        // ERRORS: immediate fatal diagnostic? should we allow you to certify random packages?
-        // You're definitely *allowed* to have unused audits, otherwise you'd be constantly deleting
-        // useful audits whenever you update your dependencies! But this might be a useful guard
-        // against typosquatting or other weird issues?
-        writeln!(
-            out,
-            "error: '{}' isn't one of your foreign packages",
-            package
-        )?;
-        panic_any(ExitPanic(-1));
+        return Err(CertifyError::NotAPackage(package));
     }
 
     let dependency_criteria = if sub_args.dependency_criteria.is_empty() {
@@ -602,13 +618,7 @@ fn do_cmd_certify(
             },
         }
     } else {
-        // ERRORS: immediate fatal diagnostic
-        writeln!(
-            out,
-            "error: couldn't guess what version of '{}' to certify, please specify",
-            package
-        )?;
-        panic_any(ExitPanic(-1));
+        return Err(CertifyError::CouldntGuessVersion(package));
     };
 
     let (username, who) = if let Some(who) = &sub_args.who {
@@ -678,8 +688,7 @@ fn do_cmd_certify(
             let input = input.trim();
             if input.is_empty() {
                 if chosen_criteria.is_empty() {
-                    writeln!(out, "no criteria chosen, aborting")?;
-                    panic_any(ExitPanic(-1));
+                    return Err(CertifyError::NoCriteriaChosen);
                 }
                 // User done selecting criteria
                 break;
@@ -781,8 +790,7 @@ fn do_cmd_certify(
                 // in and re-try the prompt if the user asks for it, in case
                 // they wrote some nice notes, but forgot to uncomment the
                 // statement.
-                writeln!(out, "error: Could not find uncommented certify statement")?;
-                panic_any(ExitPanic(-1));
+                return Err(CertifyError::CouldntFindCertifyStatement);
             }
         };
 
@@ -890,7 +898,7 @@ fn cmd_record_violation(
     out: &mut dyn Out,
     cfg: &Config,
     sub_args: &RecordViolationArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Mark a package as a violation
     let mut store = Store::acquire(cfg)?;
 
@@ -921,7 +929,7 @@ fn cmd_record_violation(
         // You're definitely *allowed* to have unused audits, otherwise you'd be constantly deleting
         // useful audits whenever you update your dependencies! But this might be a useful guard
         // against typosquatting or other weird issues?
-        return Err(eyre!(
+        return Err(miette!(
             "'{}' isn't one of your foreign packages",
             sub_args.package
         ));
@@ -953,7 +961,7 @@ fn cmd_add_unaudited(
     _out: &mut dyn Out,
     cfg: &Config,
     sub_args: &AddUnauditedArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Add an unaudited entry
     let mut store = Store::acquire(cfg)?;
 
@@ -988,7 +996,7 @@ fn cmd_add_unaudited(
         // You're definitely *allowed* to have unused audits, otherwise you'd be constantly deleting
         // useful audits whenever you update your dependencies! But this might be a useful guard
         // against typosquatting or other weird issues?
-        return Err(eyre!(
+        return Err(miette!(
             "'{}' isn't one of your foreign packages",
             sub_args.package
         ));
@@ -1015,7 +1023,11 @@ fn cmd_add_unaudited(
     Ok(())
 }
 
-fn cmd_suggest(out: &mut dyn Out, cfg: &Config, _sub_args: &SuggestArgs) -> Result<(), VetError> {
+fn cmd_suggest(
+    out: &mut dyn Out,
+    cfg: &Config,
+    _sub_args: &SuggestArgs,
+) -> Result<(), miette::Report> {
     // Run the checker to validate that the current set of deps is covered by the current cargo vet store
     trace!("suggesting...");
     let suggest_store = Store::acquire(cfg)?.clone_for_suggest();
@@ -1034,7 +1046,9 @@ fn cmd_suggest(out: &mut dyn Out, cfg: &Config, _sub_args: &SuggestArgs) -> Resu
     );
     let suggest = report.compute_suggest(cfg, network.as_ref(), true)?;
     match cfg.cli.output_format {
-        OutputFormat::Human => report.print_suggest_human(out, cfg, suggest.as_ref())?,
+        OutputFormat::Human => report
+            .print_suggest_human(out, cfg, suggest.as_ref())
+            .into_diagnostic()?,
         OutputFormat::Json => report.print_json(out, cfg, suggest.as_ref())?,
     }
 
@@ -1045,7 +1059,7 @@ fn cmd_regenerate_unaudited(
     _out: &mut dyn Out,
     cfg: &Config,
     _sub_args: &RegenerateUnauditedArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Run the checker to validate that the current set of deps is covered by the current cargo vet store
     trace!("regenerating unaudited...");
     let mut store = Store::acquire(cfg)?;
@@ -1063,7 +1077,7 @@ pub fn minimize_unaudited(
     cfg: &Config,
     store: &mut Store,
     network: Option<&Network>,
-) -> Result<(), VetError> {
+) -> Result<(), MinimizeUnauditedError> {
     // Set the unaudited entries to nothing
     let old_unaudited = mem::take(&mut store.config.unaudited);
 
@@ -1152,10 +1166,7 @@ pub fn minimize_unaudited(
     } else if let Conclusion::Success(_) = report.conclusion {
         SortedMap::new()
     } else {
-        // ERRORS: immediate fatal diagnostic
-        return Err(eyre::eyre!(
-            "error: regenerate-unaudited failed for unknown reason"
-        ));
+        return Err(MinimizeUnauditedError::Unknown);
     };
 
     // Alright there's the new unaudited
@@ -1164,7 +1175,11 @@ pub fn minimize_unaudited(
     Ok(())
 }
 
-fn cmd_diff(out: &mut dyn Out, cfg: &PartialConfig, sub_args: &DiffArgs) -> Result<(), VetError> {
+fn cmd_diff(
+    out: &mut dyn Out,
+    cfg: &PartialConfig,
+    sub_args: &DiffArgs,
+) -> Result<(), miette::Report> {
     let cache = Cache::acquire(cfg)?;
     let network = Network::acquire(cfg);
 
@@ -1180,23 +1195,26 @@ fn cmd_diff(out: &mut dyn Out, cfg: &PartialConfig, sub_args: &DiffArgs) -> Resu
         out,
         "fetching {} {} and {} ...",
         sub_args.package, sub_args.version1, sub_args.version2,
-    )?;
-
-    let (fetched1, fetched2) = tokio::runtime::Handle::current().block_on(async {
-        tokio::try_join!(
-            cache.fetch_package(network.as_ref(), package, &sub_args.version1),
-            cache.fetch_package(network.as_ref(), package, &sub_args.version2)
-        )
-    })?;
+    )
+    .into_diagnostic()?;
+
+    let (fetched1, fetched2) = tokio::runtime::Handle::current()
+        .block_on(async {
+            tokio::try_join!(
+                cache.fetch_package(network.as_ref(), package, &sub_args.version1),
+                cache.fetch_package(network.as_ref(), package, &sub_args.version2)
+            )
+        })
+        .into_diagnostic()?;
 
-    writeln!(out)?;
+    writeln!(out).into_diagnostic()?;
 
     diff_crate(out, cfg, &fetched1, &fetched2)?;
 
     Ok(())
 }
 
-fn cmd_vet(out: &mut dyn Out, cfg: &Config) -> Result<(), VetError> {
+fn cmd_vet(out: &mut dyn Out, cfg: &Config) -> Result<(), miette::Report> {
     // Run the checker to validate that the current set of deps is covered by the current cargo vet store
     trace!("vetting...");
 
@@ -1210,9 +1228,7 @@ fn cmd_vet(out: &mut dyn Out, cfg: &Config) -> Result<(), VetError> {
         }
 
         // Check if any of our first-parties are in the crates.io registry
-        if check_audit_as_crates_io(out, cfg, &store).is_err() {
-            panic_any(ExitPanic(-1));
-        }
+        check_audit_as_crates_io(cfg, &store)?;
     }
 
     // DO THE THING!!!!
@@ -1228,7 +1244,9 @@ fn cmd_vet(out: &mut dyn Out, cfg: &Config) -> Result<(), VetError> {
     );
     let suggest = report.compute_suggest(cfg, network.as_ref(), true)?;
     match cfg.cli.output_format {
-        OutputFormat::Human => report.print_human(out, cfg, suggest.as_ref())?,
+        OutputFormat::Human => report
+            .print_human(out, cfg, suggest.as_ref())
+            .into_diagnostic()?,
         OutputFormat::Json => report.print_json(out, cfg, suggest.as_ref())?,
     }
 
@@ -1248,7 +1266,7 @@ fn cmd_fetch_imports(
     out: &mut dyn Out,
     cfg: &Config,
     _sub_args: &FetchImportsArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     trace!("fetching imports...");
 
     let mut store = Store::acquire(cfg)?;
@@ -1268,7 +1286,8 @@ fn cmd_fetch_imports(
     writeln!(
         out,
         "warning: ran fetch-imports with --locked, this won't do anything!"
-    )?;
+    )
+    .into_diagnostic()?;
 
     Ok(())
 }
@@ -1277,20 +1296,20 @@ fn cmd_dump_graph(
     out: &mut dyn Out,
     cfg: &Config,
     sub_args: &DumpGraphArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Dump a mermaid-js graph
     trace!("dumping...");
 
     let graph = resolver::DepGraph::new(&cfg.metadata, cfg.cli.filter_graph.as_ref(), None);
     match cfg.cli.output_format {
-        OutputFormat::Human => graph.print_mermaid(out, sub_args)?,
-        OutputFormat::Json => serde_json::to_writer_pretty(out, &graph.nodes)?,
+        OutputFormat::Human => graph.print_mermaid(out, sub_args).into_diagnostic()?,
+        OutputFormat::Json => serde_json::to_writer_pretty(out, &graph.nodes).into_diagnostic()?,
     }
 
     Ok(())
 }
 
-fn cmd_fmt(_out: &mut dyn Out, cfg: &Config, _sub_args: &FmtArgs) -> Result<(), VetError> {
+fn cmd_fmt(_out: &mut dyn Out, cfg: &Config, _sub_args: &FmtArgs) -> Result<(), miette::Report> {
     // Reformat all the files (just load and store them, formatting is implicit).
     trace!("formatting...");
     let store = Store::acquire(cfg)?;
@@ -1302,7 +1321,7 @@ fn cmd_accept_criteria_change(
     _out: &mut dyn Out,
     _cfg: &Config,
     _sub_args: &AcceptCriteriaChangeArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     // Accept changes that a foreign audits.toml made to their criteria.
     trace!("accepting...");
 
@@ -1314,18 +1333,19 @@ fn cmd_help_md(
     out: &mut dyn Out,
     _cfg: &PartialConfig,
     _sub_args: &HelpMarkdownArgs,
-) -> Result<(), VetError> {
+) -> Result<(), miette::Report> {
     let app_name = "cargo-vet";
     let pretty_app_name = "cargo vet";
     // Make a new App to get the help message this time.
 
-    writeln!(out, "# {pretty_app_name} CLI manual")?;
-    writeln!(out)?;
+    writeln!(out, "# {pretty_app_name} CLI manual").into_diagnostic()?;
+    writeln!(out).into_diagnostic()?;
     writeln!(
         out,
         "> This manual can be regenerated with `{pretty_app_name} help-markdown`"
-    )?;
-    writeln!(out)?;
+    )
+    .into_diagnostic()?;
+    writeln!(out).into_diagnostic()?;
 
     let mut fake_cli = FakeCli::command();
     let full_command = fake_cli.get_subcommands_mut().next().unwrap();
@@ -1346,13 +1366,13 @@ fn cmd_help_md(
 
         if is_full_command {
             pretty_subcommand_name = String::new();
-            writeln!(out, "Version: `{version_line}`")?;
-            writeln!(out)?;
+            writeln!(out, "Version: `{version_line}`").into_diagnostic()?;
+            writeln!(out).into_diagnostic()?;
         } else {
             pretty_subcommand_name = format!("{pretty_app_name} {subcommand_name} ");
             // Give subcommands some breathing room
-            writeln!(out, "<br><br><br>")?;
-            writeln!(out, "## {pretty_subcommand_name}")?;
+            writeln!(out, "<br><br><br>").into_diagnostic()?;
+            writeln!(out, "## {pretty_subcommand_name}").into_diagnostic()?;
         }
 
         let mut in_subcommands_listing = false;
@@ -1366,9 +1386,9 @@ fn cmd_help_md(
                         in_subcommands_listing = heading == "SUBCOMMANDS";
                         in_usage = heading == "USAGE";
 
-                        writeln!(out, "### {pretty_subcommand_name}{heading}")?;
+                        writeln!(out, "### {pretty_subcommand_name}{heading}").into_diagnostic()?;
                     } else {
-                        writeln!(out, "### {heading}")?;
+                        writeln!(out, "### {heading}").into_diagnostic()?;
                     }
                     continue;
                 }
@@ -1380,7 +1400,8 @@ fn cmd_help_md(
                 write!(
                     out,
                     "* [{own_subcommand_name}](#{app_name}-{own_subcommand_name}): "
-                )?;
+                )
+                .into_diagnostic()?;
                 continue;
             }
             // The rest is indented, get rid of that
@@ -1388,28 +1409,28 @@ fn cmd_help_md(
 
             // Usage strings get wrapped in full code blocks
             if in_usage && line.starts_with(pretty_app_name) {
-                writeln!(out, "```")?;
-                writeln!(out, "{line}")?;
-                writeln!(out, "```")?;
+                writeln!(out, "```").into_diagnostic()?;
+                writeln!(out, "{line}").into_diagnostic()?;
+                writeln!(out, "```").into_diagnostic()?;
                 continue;
             }
 
             // argument names are subheadings
             if line.starts_with('-') || line.starts_with('<') {
-                writeln!(out, "#### `{line}`")?;
+                writeln!(out, "#### `{line}`").into_diagnostic()?;
                 continue;
             }
 
             // escape default/value strings
             if line.starts_with('[') {
-                writeln!(out, "\\{line}  ")?;
+                writeln!(out, "\\{line}  ").into_diagnostic()?;
                 continue;
             }
 
             // Normal paragraph text
-            writeln!(out, "{line}")?;
+            writeln!(out, "{line}").into_diagnostic()?;
         }
-        writeln!(out)?;
+        writeln!(out).into_diagnostic()?;
 
         todo.extend(command.get_subcommands_mut());
         is_full_command = false;
@@ -1418,7 +1439,7 @@ fn cmd_help_md(
     Ok(())
 }
 
-fn cmd_gc(out: &mut dyn Out, cfg: &PartialConfig, sub_args: &GcArgs) -> Result<(), VetError> {
+fn cmd_gc(out: &mut dyn Out, cfg: &PartialConfig, sub_args: &GcArgs) -> Result<(), miette::Report> {
     let cache = Cache::acquire(cfg)?;
 
     if sub_args.clean {
@@ -1426,16 +1447,17 @@ fn cmd_gc(out: &mut dyn Out, cfg: &PartialConfig, sub_args: &GcArgs) -> Result<(
             out,
             "cleaning entire contents of cache directory: {}",
             cfg.cache_dir.display()
-        )?;
-        cache.clean_sync()?;
+        )
+        .into_diagnostic()?;
+        cache.clean_sync().into_diagnostic()?;
         return Ok(());
     }
 
     if sub_args.max_package_age_days.is_nan() {
-        return Err(eyre!("max package age cannot be NaN"));
+        return Err(miette!("max package age cannot be NaN"));
     }
     if sub_args.max_package_age_days < 0.0 {
-        return Err(eyre!("max package age cannot be negative"));
+        return Err(miette!("max package age cannot be negative"));
     }
 
     cache.gc_sync(DURATION_DAY.mul_f64(sub_args.max_package_age_days));
@@ -1449,7 +1471,7 @@ fn diff_crate(
     _cfg: &PartialConfig,
     version1: &Path,
     version2: &Path,
-) -> Result<(), VetError> {
+) -> Result<(), DiffError> {
     // ERRORS: arguably this is all proper fallible, but it would be fatal to
     // `cargo vet diff`, the primary consumer, to not be able to diff
 
@@ -1461,14 +1483,15 @@ fn diff_crate(
         .arg("--no-index")
         .arg(version1)
         .arg(version2)
-        .status()?;
+        .status()
+        .map_err(CommandError::CommandFailed)?;
 
     let status = status.code().unwrap();
 
     // 0 = empty
     // 1 = some diff
     if status != 0 && status != 1 {
-        return Err(eyre::eyre!("git diff failed!\n {}", status));
+        Err(CommandError::BadStatus(status))?;
     }
 
     Ok(())
@@ -1479,47 +1502,27 @@ struct UserInfo {
     email: String,
 }
 
-fn get_user_info() -> Result<UserInfo, VetError> {
-    // ERRORS: this is all properly fallible internal workings
-    let username = {
-        let out = std::process::Command::new("git")
-            .arg("config")
-            .arg("--get")
-            .arg("user.name")
-            .output()?;
-
-        if !out.status.success() {
-            return Err(eyre::eyre!(
-                "could not get user.name from git!\nout:\n{}\nstderr:\n{}",
-                String::from_utf8(out.stdout).unwrap(),
-                String::from_utf8(out.stderr).unwrap()
-            ));
-        }
-        String::from_utf8(out.stdout)?
-    };
-
-    let email = {
+fn get_user_info() -> Result<UserInfo, UserInfoError> {
+    fn get_git_config(value_name: &str) -> Result<String, CommandError> {
         let out = std::process::Command::new("git")
             .arg("config")
             .arg("--get")
-            .arg("user.email")
-            .output()?;
+            .arg(value_name)
+            .output()
+            .map_err(CommandError::CommandFailed)?;
 
         if !out.status.success() {
-            return Err(eyre::eyre!(
-                "could not get user.email from git!\nout:\n{}\nstderr:\n{}",
-                String::from_utf8(out.stdout).unwrap(),
-                String::from_utf8(out.stderr).unwrap()
-            ));
+            return Err(CommandError::BadStatus(out.status.code().unwrap()));
         }
+        String::from_utf8(out.stdout)
+            .map(|s| s.trim().to_string())
+            .map_err(CommandError::BadOutput)
+    }
 
-        String::from_utf8(out.stdout)?
-    };
+    let username = get_git_config("user.name").map_err(UserInfoError::UserCommandFailed)?;
+    let email = get_git_config("user.email").map_err(UserInfoError::EmailCommandFailed)?;
 
-    Ok(UserInfo {
-        username: username.trim().to_string(),
-        email: email.trim().to_string(),
-    })
+    Ok(UserInfo { username, email })
 }
 
 async fn eula_for_criteria(
@@ -1570,11 +1573,12 @@ async fn eula_for_criteria(
     // If we get here then there must be a URL, try to fetch it. If it fails, just print the URL
     let url = Url::parse(criteria_entry.description_url.as_ref().unwrap()).unwrap();
     if let Some(network) = network {
-        if let Ok(eula) = network
-            .download(url.clone())
-            .await
-            .and_then(|bytes| Ok(String::from_utf8(bytes)?))
-        {
+        if let Ok(eula) = network.download(url.clone()).await.and_then(|bytes| {
+            String::from_utf8(bytes).map_err(|error| DownloadError::InvalidText {
+                url: url.clone(),
+                error,
+            })
+        }) {
             return eula;
         }
     }
@@ -1609,12 +1613,10 @@ fn first_party_packages_strict<'a>(
         .filter(move |package| !package.is_third_party(&empty_policy))
 }
 
-fn check_audit_as_crates_io(
-    out: &mut dyn Out,
-    cfg: &Config,
-    store: &Store,
-) -> Result<(), VetError> {
-    let cache = Cache::acquire(cfg)?;
+fn check_audit_as_crates_io(cfg: &Config, store: &Store) -> Result<(), AuditAsErrors> {
+    let cache = Cache::acquire(cfg).map_err(|e| AuditAsErrors {
+        errors: vec![AuditAsError::CacheAcquire(e)],
+    })?;
     let mut needs_audit_as_entry = vec![];
     let mut shouldnt_be_audit_as = vec![];
 
@@ -1634,7 +1636,10 @@ fn check_audit_as_crates_io(
                 // We found a version of this package in the registry!
                 if audit_policy == None {
                     // At this point, having no policy is an error
-                    needs_audit_as_entry.push(package);
+                    needs_audit_as_entry.push(NeedsAuditAsError {
+                        package: package.name.clone(),
+                        version: package.version.clone(),
+                    });
                 }
                 // Now that we've found a version match, we're done with this package
                 continue 'packages;
@@ -1644,43 +1649,27 @@ fn check_audit_as_crates_io(
         // If we reach this point, then we couldn't find a matching package in the registry,
         // So any `audit-as-crates-io = true` is an error that should be corrected
         if audit_policy == Some(true) {
-            shouldnt_be_audit_as.push(package);
+            shouldnt_be_audit_as.push(ShouldntBeAuditAsError {
+                package: package.name.clone(),
+                version: package.version.clone(),
+            });
         }
     }
 
-    // ERRORS: these are all fatal diagnostics, but they are batched
-    if !needs_audit_as_entry.is_empty() {
-        writeln!(
-            out,
-            "error: some non-crates.io-fetched packages match published crates.io versions"
-        )?;
-        for package in &needs_audit_as_entry {
-            writeln!(out, "  {}:{}", package.name, package.version)?;
+    if !needs_audit_as_entry.is_empty() || !shouldnt_be_audit_as.is_empty() {
+        let mut errors = vec![];
+        if !needs_audit_as_entry.is_empty() {
+            errors.push(AuditAsError::NeedsAuditAs(NeedsAuditAsErrors {
+                errors: needs_audit_as_entry,
+            }));
         }
-        writeln!(out)?;
-        writeln!(
-            out,
-            "All of these must have a `policy.*.audit-as-crates-io` entry"
-        )?;
-    }
-
-    if !shouldnt_be_audit_as.is_empty() {
-        writeln!(
-            out,
-            "error: some audit-as-crates-io packages don't match published crates.io versions"
-        )?;
-        for package in &shouldnt_be_audit_as {
-            writeln!(out, "  {}:{}", package.name, package.version)?;
+        if !shouldnt_be_audit_as.is_empty() {
+            errors.push(AuditAsError::ShouldntBeAuditAs(ShouldntBeAuditAsErrors {
+                errors: shouldnt_be_audit_as,
+            }));
         }
-        writeln!(
-            out,
-            "Either remove audit-as-crates-io from their policies or set it to false"
-        )?;
+        return Err(AuditAsErrors { errors });
     }
 
-    if !shouldnt_be_audit_as.is_empty() || !needs_audit_as_entry.is_empty() {
-        Err(eyre!("temp error"))
-    } else {
-        Ok(())
-    }
+    Ok(())
 }
diff --git a/src/network.rs b/src/network.rs
index 00eae03..9ce2d7e 100644
--- a/src/network.rs
+++ b/src/network.rs
@@ -9,15 +9,14 @@
 
 use std::{
     ffi::{OsStr, OsString},
-    path::Path,
+    path::{Path, PathBuf},
     time::Duration,
 };
 
-use eyre::Context;
 use reqwest::{Client, Url};
 use tokio::io::AsyncWriteExt;
 
-use crate::{PartialConfig, VetError};
+use crate::{errors::DownloadError, PartialConfig};
 
 pub struct Network {
     /// The HTTP client all requests go through
@@ -54,10 +53,21 @@ impl Network {
     }
 
     /// Download a file and persist it to disk
-    pub async fn download_and_persist(&self, url: Url, persist_to: &Path) -> Result<(), VetError> {
-        let download_tmp_path = OsString::from_iter([persist_to.as_os_str(), OsStr::new(".part")]);
+    pub async fn download_and_persist(
+        &self,
+        url: Url,
+        persist_to: &Path,
+    ) -> Result<(), DownloadError> {
+        let download_tmp_path = PathBuf::from(OsString::from_iter([
+            persist_to.as_os_str(),
+            OsStr::new(".part"),
+        ]));
         {
-            let _permit = self.connection_semaphore.acquire().await?;
+            let _permit = self
+                .connection_semaphore
+                .acquire()
+                .await
+                .expect("Semaphore dropped?!");
 
             let mut res = self
                 .client
@@ -65,14 +75,34 @@ impl Network {
                 .send()
                 .await
                 .and_then(|res| res.error_for_status())
-                .wrap_err_with(|| format!("Failed to download {}", url))?;
-
-            let mut download_tmp = tokio::fs::File::create(&download_tmp_path)
-                .await
-                .wrap_err("could not create tempfile for download")?;
-            while let Some(chunk) = res.chunk().await? {
+                .map_err(|error| DownloadError::FailedToStartDownload {
+                    url: url.clone(),
+                    error,
+                })?;
+
+            let mut download_tmp =
+                tokio::fs::File::create(&download_tmp_path)
+                    .await
+                    .map_err(|error| DownloadError::FailedToCreateDownload {
+                        target: download_tmp_path.clone(),
+                        error,
+                    })?;
+            while let Some(chunk) =
+                res.chunk()
+                    .await
+                    .map_err(|error| DownloadError::FailedToReadDownload {
+                        url: url.clone(),
+                        error,
+                    })?
+            {
                 let network_bytes = &chunk[..];
-                download_tmp.write_all(network_bytes).await?;
+                download_tmp
+                    .write_all(network_bytes)
+                    .await
+                    .map_err(|error| DownloadError::FailedToWriteDownload {
+                        target: download_tmp_path.clone(),
+                        error,
+                    })?;
             }
         }
 
@@ -81,12 +111,10 @@ impl Network {
             Ok(()) => {}
             Err(err) => {
                 let _ = tokio::fs::remove_file(&download_tmp_path).await;
-                return Err(err).wrap_err_with(|| {
-                    format!(
-                        "Couldn't swap download into final location: {}",
-                        persist_to.display()
-                    )
-                });
+                return Err(err).map_err(|error| DownloadError::FailedToFinalizeDownload {
+                    target: persist_to.to_owned(),
+                    error,
+                })?;
             }
         }
 
@@ -94,8 +122,12 @@ impl Network {
     }
 
     /// Download a file into memory
-    pub async fn download(&self, url: Url) -> Result<Vec<u8>, VetError> {
-        let _permit = self.connection_semaphore.acquire().await?;
+    pub async fn download(&self, url: Url) -> Result<Vec<u8>, DownloadError> {
+        let _permit = self
+            .connection_semaphore
+            .acquire()
+            .await
+            .expect("Semaphore dropped?!");
 
         let mut res = self
             .client
@@ -103,10 +135,20 @@ impl Network {
             .send()
             .await
             .and_then(|res| res.error_for_status())
-            .wrap_err_with(|| format!("Failed to download {}", url))?;
+            .map_err(|error| DownloadError::FailedToStartDownload {
+                url: url.clone(),
+                error,
+            })?;
 
         let mut output = vec![];
-        while let Some(chunk) = res.chunk().await? {
+        while let Some(chunk) =
+            res.chunk()
+                .await
+                .map_err(|error| DownloadError::FailedToReadDownload {
+                    url: url.clone(),
+                    error,
+                })?
+        {
             let network_bytes = &chunk[..];
             output.extend_from_slice(network_bytes);
         }
diff --git a/src/resolver.rs b/src/resolver.rs
index f56adf8..68e4f93 100644
--- a/src/resolver.rs
+++ b/src/resolver.rs
@@ -61,10 +61,12 @@
 use cargo_metadata::{DependencyKind, Metadata, Node, PackageId, Version};
 use core::fmt;
 use futures_util::future::join_all;
+use miette::IntoDiagnostic;
 use serde::Serialize;
 use serde_json::json;
 use tracing::{error, trace, trace_span, warn};
 
+use crate::errors::SuggestError;
 use crate::format::{
     self, AuditKind, CriteriaName, CriteriaStr, Delta, DiffStat, ImportName, PackageName,
     PackageStr, PolicyEntry, UnauditedDependency,
@@ -74,7 +76,7 @@ use crate::network::Network;
 use crate::out::Out;
 use crate::{
     AuditEntry, Cache, Config, CriteriaEntry, DumpGraphArgs, GraphFilter, GraphFilterProperty,
-    GraphFilterQuery, PackageExt, Store, VetError,
+    GraphFilterQuery, PackageExt, Store,
 };
 
 /// A report of the results of running `resolve`.
@@ -1035,7 +1037,7 @@ impl<'a> DepGraph<'a> {
         &self,
         out: &mut dyn Out,
         sub_args: &DumpGraphArgs,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         use crate::DumpGraphDepth::*;
         let depth = sub_args.depth;
 
@@ -2326,7 +2328,7 @@ impl<'a> ResolveReport<'a> {
         cfg: &Config,
         network: Option<&Network>,
         allow_deltas: bool,
-    ) -> Result<Option<Suggest>, VetError> {
+    ) -> Result<Option<Suggest>, SuggestError> {
         let _suggest_span = trace_span!("suggest").entered();
         let fail = if let Conclusion::FailForVet(fail) = &self.conclusion {
             fail
@@ -2568,7 +2570,7 @@ impl<'a> ResolveReport<'a> {
         out: &mut dyn Out,
         cfg: &Config,
         suggest: Option<&Suggest>,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         match &self.conclusion {
             Conclusion::Success(res) => res.print_human(out, self, cfg),
             Conclusion::FailForViolationConflict(res) => res.print_human(out, self, cfg),
@@ -2582,7 +2584,7 @@ impl<'a> ResolveReport<'a> {
         out: &mut dyn Out,
         _cfg: &Config,
         suggest: Option<&Suggest>,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         if let Some(suggest) = suggest {
             suggest.print_human(out, self)?;
         } else {
@@ -2598,7 +2600,7 @@ impl<'a> ResolveReport<'a> {
         out: &mut dyn Out,
         _cfg: &Config,
         suggest: Option<&Suggest>,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), miette::Report> {
         let result = match &self.conclusion {
             Conclusion::Success(success) => {
                 let json_package = |pkgidx: &PackageIdx| {
@@ -2654,7 +2656,7 @@ impl<'a> ResolveReport<'a> {
             }
         };
 
-        serde_json::to_writer_pretty(out, &result)?;
+        serde_json::to_writer_pretty(out, &result).into_diagnostic()?;
 
         Ok(())
     }
@@ -2666,7 +2668,7 @@ impl Success {
         out: &mut dyn Out,
         report: &ResolveReport,
         _cfg: &Config,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         let fully_audited_count = self.vetted_fully.len();
         let partially_audited_count: usize = self.vetted_partially.len();
         let unaudited_count = self.vetted_with_unaudited.len();
@@ -2727,7 +2729,11 @@ impl Success {
 }
 
 impl Suggest {
-    pub fn print_human(&self, out: &mut dyn Out, report: &ResolveReport) -> Result<(), VetError> {
+    pub fn print_human(
+        &self,
+        out: &mut dyn Out,
+        report: &ResolveReport,
+    ) -> Result<(), std::io::Error> {
         for (criteria, suggestions) in &self.suggestions_by_criteria {
             writeln!(out, "recommended audits for {}:", criteria)?;
 
@@ -2803,7 +2809,7 @@ impl FailForVet {
         report: &ResolveReport,
         _cfg: &Config,
         suggest: Option<&Suggest>,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         writeln!(out, "Vetting Failed!")?;
         writeln!(out)?;
         writeln!(out, "{} unvetted dependencies:", self.failures.len())?;
@@ -2862,7 +2868,7 @@ impl FailForViolationConflict {
         out: &mut dyn Out,
         report: &ResolveReport,
         _cfg: &Config,
-    ) -> Result<(), VetError> {
+    ) -> Result<(), std::io::Error> {
         writeln!(out, "Violations Found!")?;
 
         for (&pkgidx, violations) in &self.violations {
@@ -2899,7 +2905,7 @@ impl FailForViolationConflict {
         fn print_unaudited_entry(
             out: &mut dyn Out,
             entry: &UnauditedDependency,
-        ) -> Result<(), VetError> {
+        ) -> Result<(), std::io::Error> {
             writeln!(out, "unaudited {}", entry.version)?;
             writeln!(out, "      criteria: {:?}", entry.criteria)?;
             if let Some(notes) = &entry.notes {
@@ -2912,7 +2918,7 @@ impl FailForViolationConflict {
             out: &mut dyn Out,
             source: &AuditSource,
             entry: &AuditEntry,
-        ) -> Result<(), VetError> {
+        ) -> Result<(), std::io::Error> {
             match source {
                 AuditSource::OwnAudits => write!(out, "own ")?,
                 AuditSource::Foreign(name) => write!(out, "foreign ({name}) ")?,
diff --git a/src/storage.rs b/src/storage.rs
index 4692d95..deae943 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -10,7 +10,6 @@ use std::{
 
 use cargo_metadata::Version;
 use crates_index::Index;
-use eyre::{eyre, Context};
 use flate2::read::GzDecoder;
 use futures_util::future::try_join_all;
 use reqwest::Url;
@@ -19,6 +18,12 @@ use tar::Archive;
 use tracing::{error, info, log::warn, trace};
 
 use crate::{
+    errors::{
+        CacheAcquireError, CacheCommitError, CommandError, DiffError, FetchAndDiffError,
+        FetchAuditError, FetchError, FlockError, JsonParseError, LoadJsonError, LoadTomlError,
+        StoreAcquireError, StoreCommitError, StoreCreateError, StoreJsonError, StoreTomlError,
+        StoreValidateErrors, TomlParseError, UnpackError,
+    },
     flock::{FileLock, Filesystem},
     format::{
         AuditsFile, CommandHistory, ConfigFile, Delta, DiffCache, DiffStat, FastMap, FetchCommand,
@@ -27,7 +32,7 @@ use crate::{
     network::Network,
     resolver,
     serialization::to_formatted_toml,
-    Config, PartialConfig, VetError,
+    Config, PartialConfig,
 };
 
 // tmp cache for various shenanigans
@@ -69,7 +74,7 @@ struct StoreLock {
 }
 
 impl StoreLock {
-    fn new(store: &Filesystem) -> Result<Self, VetError> {
+    fn new(store: &Filesystem) -> Result<Self, FlockError> {
         Ok(StoreLock {
             config: store.open_rw(CONFIG_TOML, "vet store")?,
         })
@@ -119,9 +124,9 @@ pub struct Store {
 
 impl Store {
     /// Create a new store (files will be completely empty, must be committed for files to be created)
-    pub fn create(cfg: &Config) -> Result<Self, VetError> {
+    pub fn create(cfg: &Config) -> Result<Self, StoreCreateError> {
         let root = cfg.metacfg.store_path();
-        root.create_dir()?;
+        root.create_dir().map_err(StoreCreateError::CouldntCreate)?;
 
         let lock = StoreLock::new(&root)?;
 
@@ -149,7 +154,7 @@ impl Store {
     }
 
     /// Acquire an existing store
-    pub fn acquire(cfg: &Config) -> Result<Self, VetError> {
+    pub fn acquire(cfg: &Config) -> Result<Self, StoreAcquireError> {
         let root = cfg.metacfg.store_path();
 
         // Before we do anything else, acquire an exclusive lock on the
@@ -210,7 +215,7 @@ impl Store {
     }
 
     /// Commit the store's contents back to disk
-    pub fn commit(self) -> Result<(), VetError> {
+    pub fn commit(self) -> Result<(), StoreCommitError> {
         // TODO: make this truly transactional?
         // (With a dir rename? Does that work with the lock? Fine because it's already closed?)
         if let Some(lock) = self.lock {
@@ -225,7 +230,7 @@ impl Store {
     }
 
     /// Validate the store's integrity
-    pub fn validate(&self) -> Result<(), VetError> {
+    pub fn validate(&self) -> Result<(), StoreValidateErrors> {
         // ERRORS: ideally these are all gathered diagnostics, want to report as many errors
         // at once as possible!
 
@@ -249,11 +254,11 @@ impl Store {
     }
 
     /// Fetch foreign audits, only call this is we're not --locked
-    pub async fn fetch_foreign_audits(&mut self, network: &Network) -> Result<(), VetError> {
+    pub async fn fetch_foreign_audits(&mut self, network: &Network) -> Result<(), FetchAuditError> {
         let new_imports = ImportsFile {
             audits: try_join_all(self.config.imports.iter().map(|(name, import)| async {
                 let audit_file = fetch_foreign_audit(network, name, &import.url).await?;
-                Ok::<_, VetError>((name.clone(), audit_file))
+                Ok::<_, FetchAuditError>((name.clone(), audit_file))
             }))
             .await?
             .into_iter()
@@ -275,15 +280,15 @@ async fn fetch_foreign_audit(
     network: &Network,
     name: &str,
     url: &str,
-) -> Result<AuditsFile, VetError> {
-    let parsed_url =
-        Url::parse(url).wrap_err_with(|| format!("Invalid url for audit {name} @ {url}"))?;
-    let audit_bytes = network
-        .download(parsed_url)
-        .await
-        .wrap_err_with(|| format!("Could not import audit {name} @ {url}"))?;
-    let audit_file: AuditsFile = toml_edit::de::from_slice(&audit_bytes)
-        .wrap_err_with(|| format!("Could not parse {name} @ {url}"))?;
+) -> Result<AuditsFile, FetchAuditError> {
+    let parsed_url = Url::parse(url).map_err(|error| FetchAuditError::InvalidUrl {
+        import_url: url.to_owned(),
+        import_name: name.to_owned(),
+        error,
+    })?;
+    let audit_bytes = network.download(parsed_url).await?;
+    let audit_file: AuditsFile =
+        toml_edit::de::from_slice(&audit_bytes).map_err(|error| TomlParseError { error })?;
     Ok(audit_file)
 }
 
@@ -348,7 +353,7 @@ impl Drop for Cache {
         let state = self.state.get_mut().unwrap();
         if let Some(diff_cache_path) = &self.diff_cache_path {
             // Write back the diff_cache
-            if let Err(err) = || -> Result<(), VetError> {
+            if let Err(err) = || -> Result<(), CacheCommitError> {
                 store_diff_cache(
                     File::create(diff_cache_path)?,
                     mem::take(&mut state.diff_cache),
@@ -360,7 +365,7 @@ impl Drop for Cache {
         }
         if let Some(command_history_path) = &self.command_history_path {
             // Write back the command_history
-            if let Err(err) = || -> Result<(), VetError> {
+            if let Err(err) = || -> Result<(), CacheCommitError> {
                 store_command_history(
                     File::create(command_history_path)?,
                     mem::take(&mut state.command_history),
@@ -376,7 +381,7 @@ impl Drop for Cache {
 
 impl Cache {
     /// Acquire the cache
-    pub fn acquire(cfg: &PartialConfig) -> Result<Self, VetError> {
+    pub fn acquire(cfg: &PartialConfig) -> Result<Self, CacheAcquireError> {
         if cfg.mock_cache {
             // We're in unit tests, everything should be mocked and not touch real caches
             return Ok(Cache {
@@ -397,31 +402,29 @@ impl Cache {
 
         // Make sure the cache directory exists, and acquire an exclusive lock on it.
         let root = cfg.cache_dir.clone();
-        fs::create_dir_all(&root)
-            .wrap_err_with(|| format!("failed to create cache directory `{}`", root.display()))?;
+        fs::create_dir_all(&root).map_err(|error| CacheAcquireError::Root {
+            target: root.clone(),
+            error,
+        })?;
 
         let lock = Filesystem::new(root.clone()).open_rw(CACHE_VET_LOCK, "cache lock")?;
 
         let empty = root.join(CACHE_EMPTY_PACKAGE);
-        fs::create_dir_all(&empty).wrap_err_with(|| {
-            format!(
-                "failed to create cache empty directory `{}`",
-                empty.display()
-            )
+        fs::create_dir_all(&empty).map_err(|error| CacheAcquireError::Empty {
+            target: empty.clone(),
+            error,
         })?;
+
         let packages_src = root.join(CACHE_REGISTRY_SRC);
-        fs::create_dir_all(&packages_src).wrap_err_with(|| {
-            format!(
-                "failed to create package src directory `{}`",
-                packages_src.display()
-            )
+        fs::create_dir_all(&packages_src).map_err(|error| CacheAcquireError::Src {
+            target: empty.clone(),
+            error,
         })?;
+
         let packages_cache = root.join(CACHE_REGISTRY_CACHE);
-        fs::create_dir_all(&packages_cache).wrap_err_with(|| {
-            format!(
-                "failed to create package cache directory `{}`",
-                packages_cache.display()
-            )
+        fs::create_dir_all(&packages_cache).map_err(|error| CacheAcquireError::Cache {
+            target: packages_cache.clone(),
+            error,
         })?;
 
         // Setup the diff_cache.
@@ -492,7 +495,7 @@ impl Cache {
         network: Option<&Network>,
         package: PackageStr<'_>,
         version: &Version,
-    ) -> Result<PathBuf, VetError> {
+    ) -> Result<PathBuf, FetchError> {
         // Lock the mutex to extract a reference to the OnceCell which we'll use
         // to asynchronously synchronize on and fetch the package only once in a
         // single execution.
@@ -506,7 +509,7 @@ impl Cache {
                 .clone()
         };
 
-        let path = once_cell
+        let path_res: Result<_, FetchError> = once_cell
             .get_or_try_init(|| async {
                 let root = self.root.as_ref().unwrap();
 
@@ -549,20 +552,25 @@ impl Cache {
                         file
                     })
                 })
-                .await?;
+                .await
+                .expect("failed to join");
 
                 // If the file isn't in our local cache, make sure to download it.
                 let file = match cached_file {
                     Ok(file) => file,
                     Err(_) => {
-                        let network = network.ok_or_else(|| {
-                            eyre!("running as --frozen but needed to fetch {package}:{version}")
+                        let network = network.ok_or_else(|| FetchError::Frozen {
+                            package: package.to_owned(),
+                            version: version.clone(),
                         })?;
 
                         // We don't have it, so download it
-                        let url = Url::parse(&format!(
-                            "https://crates.io/api/v1/crates/{package}/{version}/download"
-                        ))?;
+                        let url =
+                            format!("https://crates.io/api/v1/crates/{package}/{version}/download");
+                        let url = Url::parse(&url).map_err(|error| FetchError::InvalidUrl {
+                            url: url.clone(),
+                            error,
+                        })?;
                         info!(
                             "downloading package {}:{} from {} to {}",
                             package,
@@ -573,7 +581,13 @@ impl Cache {
                         network.download_and_persist(url, &fetched_package).await?;
 
                         let fetched_package_ = fetched_package.clone();
-                        tokio::task::spawn_blocking(move || File::open(&fetched_package_)).await??
+                        tokio::task::spawn_blocking(move || File::open(&fetched_package_))
+                            .await
+                            .expect("failed to join")
+                            .map_err(|error| FetchError::OpenCached {
+                                target: fetched_package.clone(),
+                                error,
+                            })?
                     }
                 };
 
@@ -593,14 +607,17 @@ impl Cache {
                     tokio::task::spawn_blocking(move || {
                         unpack_package(&file, &fetched_src)
                             .map(|_| fetched_src)
-                            .wrap_err_with(|| {
-                                format!("error unpacking {}", fetched_package.display())
+                            .map_err(|error| FetchError::Unpack {
+                                src: fetched_package.clone(),
+                                error,
                             })
                     })
-                    .await?
+                    .await
+                    .expect("failed to join")
                 }
             })
-            .await?;
+            .await;
+        let path = path_res?;
         Ok(path.to_owned())
     }
 
@@ -609,8 +626,12 @@ impl Cache {
         &self,
         version1: &Path,
         version2: &Path,
-    ) -> Result<DiffStat, VetError> {
-        let _permit = self.diff_semaphore.acquire().await?;
+    ) -> Result<DiffStat, DiffError> {
+        let _permit = self
+            .diff_semaphore
+            .acquire()
+            .await
+            .expect("Semaphore dropped?!");
 
         // ERRORS: all of this is properly fallible internal workings, we can fail
         // to diffstat some packages and still produce some useful output
@@ -625,20 +646,17 @@ impl Cache {
             .arg(version1)
             .arg(version2)
             .output()
-            .await?;
+            .await
+            .map_err(CommandError::CommandFailed)?;
 
         let status = out.status.code().unwrap_or(-1);
         // 0 = empty
         // 1 = some diff
         if status != 0 && status != 1 {
-            return Err(eyre::eyre!(
-                "command failed!\nout:\n{}\nstderr:\n{}",
-                String::from_utf8(out.stdout).unwrap(),
-                String::from_utf8(out.stderr).unwrap()
-            ));
+            Err(CommandError::BadStatus(status))?;
         }
 
-        let diffstat = String::from_utf8(out.stdout)?;
+        let diffstat = String::from_utf8(out.stdout).map_err(CommandError::BadOutput)?;
 
         let count = if diffstat.is_empty() {
             0
@@ -679,7 +697,7 @@ impl Cache {
         network: Option<&Network>,
         package: PackageStr<'_>,
         delta: &Delta,
-    ) -> Result<DiffStat, VetError> {
+    ) -> Result<DiffStat, FetchAndDiffError> {
         // Lock the mutex to extract a reference to the OnceCell which we'll use
         // to asynchronously synchronize on and diff the package only once in a
         // single execution.
@@ -742,7 +760,7 @@ impl Cache {
                         .insert(delta.clone(), diffstat.clone());
                 }
 
-                Ok::<_, VetError>(diffstat)
+                Ok::<_, FetchAndDiffError>(diffstat)
             })
             .await?;
         Ok(diffstat.clone())
@@ -779,7 +797,7 @@ impl Cache {
 
     /// Remove any unrecognized files from the root of the cargo-vet cache
     /// directory.
-    async fn gc_root(&self) -> Result<(), VetError> {
+    async fn gc_root(&self) -> Result<(), io::Error> {
         let root = self.root.as_ref().unwrap();
         let mut root_entries = tokio::fs::read_dir(root).await?;
         while let Some(entry) = root_entries.next_entry().await? {
@@ -796,7 +814,7 @@ impl Cache {
 
     /// Remove all files located in the `cargo-vet/empty` directory, as it
     /// should be empty.
-    async fn gc_empty(&self) -> Result<(), VetError> {
+    async fn gc_empty(&self) -> Result<(), std::io::Error> {
         let empty = self.root.as_ref().unwrap().join(CACHE_EMPTY_PACKAGE);
         let mut empty_entries = tokio::fs::read_dir(&empty).await?;
         while let Some(entry) = empty_entries.next_entry().await? {
@@ -808,7 +826,7 @@ impl Cache {
     /// Remove any non '.crate' files from the registry cache, '.crate' files
     /// which are older than `max_package_age`, and any source directories from
     /// the registry src which no longer have a corresponding .crate.
-    async fn gc_packages(&self, max_package_age: Duration) -> Result<(), VetError> {
+    async fn gc_packages(&self, max_package_age: Duration) -> Result<(), io::Error> {
         let cache = self.root.as_ref().unwrap().join(CACHE_REGISTRY_CACHE);
         let src = self.root.as_ref().unwrap().join(CACHE_REGISTRY_SRC);
 
@@ -837,11 +855,8 @@ impl Cache {
     ///
     /// NOTE: The diff_cache and command_history files will be re-created when
     /// the cache is unlocked, however they will be empty.
-    pub async fn clean(&self) -> Result<(), VetError> {
-        let root = self
-            .root
-            .as_ref()
-            .ok_or_else(|| eyre!("cannot clean a mocked cache"))?;
+    pub async fn clean(&self) -> Result<(), io::Error> {
+        let root = self.root.as_ref().expect("cannot clean a mocked cache");
 
         // Make sure we don't write back the command history and diff cache when
         // dropping.
@@ -861,7 +876,7 @@ impl Cache {
     }
 
     /// Sync version of `clean`
-    pub fn clean_sync(&self) -> Result<(), VetError> {
+    pub fn clean_sync(&self) -> Result<(), io::Error> {
         tokio::runtime::Handle::current().block_on(self.clean())
     }
 
@@ -892,7 +907,7 @@ pub fn exact_version<'a>(
 }
 
 #[tracing::instrument(err)]
-fn unpack_package(tarball: &File, unpack_dir: &Path) -> Result<(), VetError> {
+fn unpack_package(tarball: &File, unpack_dir: &Path) -> Result<(), UnpackError> {
     // If we get here and the unpack_dir exists, this implies we had a previously failed fetch,
     // blast it away so we can have a clean slate!
     if unpack_dir.exists() {
@@ -905,10 +920,10 @@ fn unpack_package(tarball: &File, unpack_dir: &Path) -> Result<(), VetError> {
     let prefix = unpack_dir.file_name().unwrap();
     let parent = unpack_dir.parent().unwrap();
     for entry in tar.entries()? {
-        let mut entry = entry.wrap_err("failed to iterate over archive")?;
+        let mut entry = entry.map_err(UnpackError::ArchiveIterate)?;
         let entry_path = entry
             .path()
-            .wrap_err("failed to read entry path")?
+            .map_err(UnpackError::ArchiveEntry)?
             .into_owned();
 
         // We're going to unpack this tarball into the global source
@@ -918,16 +933,18 @@ fn unpack_package(tarball: &File, unpack_dir: &Path) -> Result<(), VetError> {
         // crates.io should also block uploads with these sorts of tarballs,
         // but be extra sure by adding a check here as well.
         if !entry_path.starts_with(prefix) {
-            return Err(eyre::eyre!(
-                "invalid tarball downloaded, contains \
-                    a file at {} which isn't under {}",
-                entry_path.display(),
-                prefix.to_string_lossy()
-            ));
+            return Err(UnpackError::InvalidPaths {
+                entry_path,
+                prefix: prefix.to_owned(),
+            });
         }
-        // Unpacking failed
-        let result = entry.unpack_in(parent).map_err(VetError::from);
-        result.wrap_err_with(|| format!("failed to unpack entry at `{}`", entry_path.display()))?;
+
+        entry
+            .unpack_in(parent)
+            .map_err(|error| UnpackError::Unpack {
+                entry_path: entry_path.clone(),
+                error,
+            })?;
     }
 
     // The lock file is created after unpacking so we overwrite a lock file
@@ -937,10 +954,16 @@ fn unpack_package(tarball: &File, unpack_dir: &Path) -> Result<(), VetError> {
         .read(true)
         .write(true)
         .open(&lockfile)
-        .wrap_err_with(|| format!("failed to open `{}`", lockfile.display()))?;
+        .map_err(|error| UnpackError::LockCreate {
+            target: lockfile.clone(),
+            error,
+        })?;
 
     // Write to the lock file to indicate that unpacking was successful.
-    write!(ok, "ok")?;
+    write!(ok, "ok").map_err(|error| UnpackError::LockCreate {
+        target: lockfile.clone(),
+        error,
+    })?;
 
     Ok(())
 }
@@ -955,7 +978,7 @@ async fn fetch_is_ok(fetch: &Path) -> bool {
 /// Based on the type of file for an entry, either recursively remove the
 /// directory, or remove the file. This is intended to be roughly equivalent to
 /// `rm -r`.
-async fn remove_dir_entry(entry: &tokio::fs::DirEntry) -> Result<(), VetError> {
+async fn remove_dir_entry(entry: &tokio::fs::DirEntry) -> Result<(), io::Error> {
     info!("gc: removing {}", entry.path().display());
     let file_type = entry.file_type().await?;
     if file_type.is_dir() {
@@ -993,7 +1016,7 @@ async fn should_keep_package(
     }
 }
 
-fn find_cargo_registry() -> Result<CargoRegistry, VetError> {
+fn find_cargo_registry() -> Result<CargoRegistry, crates_index::Error> {
     // ERRORS: all of this is genuinely fallible internal workings
     // but if these path adjustments don't work then something is very fundamentally wrong
 
@@ -1009,17 +1032,17 @@ fn find_cargo_registry() -> Result<CargoRegistry, VetError> {
     })
 }
 
-fn load_toml<T>(reader: impl Read) -> Result<T, VetError>
+fn load_toml<T>(reader: impl Read) -> Result<T, LoadTomlError>
 where
     T: for<'a> Deserialize<'a>,
 {
     let mut reader = BufReader::new(reader);
     let mut string = String::new();
     reader.read_to_string(&mut string)?;
-    let toml = toml_edit::de::from_str(&string)?;
+    let toml = toml_edit::de::from_str(&string).map_err(|error| TomlParseError { error })?;
     Ok(toml)
 }
-fn store_toml<T>(mut writer: impl Write, heading: &str, val: T) -> Result<(), VetError>
+fn store_toml<T>(mut writer: impl Write, heading: &str, val: T) -> Result<(), StoreTomlError>
 where
     T: Serialize,
 {
@@ -1028,17 +1051,17 @@ where
     writeln!(writer, "{}{}", heading, toml_document)?;
     Ok(())
 }
-fn load_json<T>(reader: impl Read) -> Result<T, VetError>
+fn load_json<T>(reader: impl Read) -> Result<T, LoadJsonError>
 where
     T: for<'a> Deserialize<'a>,
 {
     let mut reader = BufReader::new(reader);
     let mut string = String::new();
     reader.read_to_string(&mut string)?;
-    let json = serde_json::from_str(&string)?;
+    let json = serde_json::from_str(&string).map_err(|error| JsonParseError { error })?;
     Ok(json)
 }
-fn store_json<T>(mut writer: impl Write, val: T) -> Result<(), VetError>
+fn store_json<T>(mut writer: impl Write, val: T) -> Result<(), StoreJsonError>
 where
     T: Serialize,
 {
@@ -1048,7 +1071,7 @@ where
     Ok(())
 }
 
-fn store_audits(writer: impl Write, mut audits: AuditsFile) -> Result<(), VetError> {
+fn store_audits(writer: impl Write, mut audits: AuditsFile) -> Result<(), StoreTomlError> {
     let heading = r###"
 # cargo-vet audits file
 "###;
@@ -1060,7 +1083,7 @@ fn store_audits(writer: impl Write, mut audits: AuditsFile) -> Result<(), VetErr
     store_toml(writer, heading, audits)?;
     Ok(())
 }
-fn store_config(writer: impl Write, mut config: ConfigFile) -> Result<(), VetError> {
+fn store_config(writer: impl Write, mut config: ConfigFile) -> Result<(), StoreTomlError> {
     config
         .unaudited
         .values_mut()
@@ -1073,7 +1096,7 @@ fn store_config(writer: impl Write, mut config: ConfigFile) -> Result<(), VetErr
     store_toml(writer, heading, config)?;
     Ok(())
 }
-fn store_imports(writer: impl Write, imports: ImportsFile) -> Result<(), VetError> {
+fn store_imports(writer: impl Write, imports: ImportsFile) -> Result<(), StoreTomlError> {
     let heading = r###"
 # cargo-vet imports lock
 "###;
@@ -1081,7 +1104,7 @@ fn store_imports(writer: impl Write, imports: ImportsFile) -> Result<(), VetErro
     store_toml(writer, heading, imports)?;
     Ok(())
 }
-fn store_diff_cache(writer: impl Write, diff_cache: DiffCache) -> Result<(), VetError> {
+fn store_diff_cache(writer: impl Write, diff_cache: DiffCache) -> Result<(), StoreTomlError> {
     let heading = "";
 
     store_toml(writer, heading, diff_cache)?;
@@ -1090,7 +1113,7 @@ fn store_diff_cache(writer: impl Write, diff_cache: DiffCache) -> Result<(), Vet
 fn store_command_history(
     writer: impl Write,
     command_history: CommandHistory,
-) -> Result<(), VetError> {
+) -> Result<(), StoreJsonError> {
     store_json(writer, command_history)?;
     Ok(())
 }


Test patch:
diff --git a/src/tests/audit_as_crates_io.rs b/src/tests/audit_as_crates_io.rs
index 20ea2e7..a8382e4 100644
--- a/src/tests/audit_as_crates_io.rs
+++ b/src/tests/audit_as_crates_io.rs
@@ -1,13 +1,17 @@
 use super::*;
 
 fn get_audit_as_crates_io(cfg: &Config, store: &Store) -> String {
-    let mut output = BasicTestOutput::new();
-    let _res = crate::check_audit_as_crates_io(&mut output, cfg, store);
-    output.to_string()
+    let res = crate::check_audit_as_crates_io(cfg, store);
+    match res {
+        Ok(()) => String::new(),
+        Err(e) => format!("{:?}", miette::Report::new(e)),
+    }
 }
 
 #[test]
 fn simple_audit_as_crates_io() {
+    let _enter = TEST_RUNTIME.enter();
+
     let mock = MockMetadata::simple();
     let metadata = mock.metadata();
     let (config, audits, imports) = builtin_files_full_audited(&metadata);
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index cef7dd4..2453e0e 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -34,8 +34,33 @@ const DEFAULT_VER: u64 = 10;
 const DEFAULT_CRIT: CriteriaStr = "reviewed";
 
 lazy_static::lazy_static! {
-    static ref TEST_RUNTIME: tokio::runtime::Runtime =
-        tokio::runtime::Runtime::new().unwrap();
+    static ref TEST_RUNTIME: tokio::runtime::Runtime = {
+        let error_colors_enabled = false;
+        miette::set_hook(Box::new(move |_| {
+            let graphical_theme = if error_colors_enabled {
+                miette::GraphicalTheme::unicode()
+            } else {
+                miette::GraphicalTheme::unicode_nocolor()
+            };
+            Box::new(
+                miette::MietteHandlerOpts::new()
+                    .graphical_theme(graphical_theme)
+                    .build()
+            )
+        })).expect("Failed to initialize error handler");
+
+        tracing_subscriber::fmt::fmt()
+            .with_max_level(tracing::level_filters::LevelFilter::OFF)
+            // Toggle this on for tracing in tests
+            // .with_max_level(tracing::level_filters::LevelFilter::TRACE)
+            .with_target(false)
+            .without_time()
+            .with_writer(std::io::stderr)
+            .init();
+
+        tokio::runtime::Runtime::new().unwrap()
+    };
+
 }
 
 struct MockMetadata {
@@ -817,7 +842,7 @@ impl MockMetadata {
 }
 
 fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
-    let (mut config, mut audits, imports) = init_files(metadata, None).unwrap();
+    let (mut config, mut audits, imports) = init_files(metadata, None);
 
     // Criteria hierarchy:
     //
@@ -923,7 +948,7 @@ fn files_full_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFi
 }
 
 fn builtin_files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
-    init_files(metadata, None).unwrap()
+    init_files(metadata, None)
 }
 
 fn builtin_files_no_unaudited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
@@ -1078,14 +1103,6 @@ impl<'a> Out for BasicTestOutput<'a> {
     }
 }
 
-fn _init_trace_logger() {
-    tracing_subscriber::fmt::fmt()
-        .with_target(false)
-        .without_time()
-        .with_writer(std::io::stderr)
-        .init();
-}
-
 // TESTING BACKLOG:
 //
 // * custom policies
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
index 9dd266e..0662b49 100644
--- a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__simple-audit-as-crates-io.snap
@@ -2,8 +2,11 @@
 source: src/tests/audit_as_crates_io.rs
 expression: output
 ---
-error: some non-crates.io-fetched packages match published crates.io versions
-  first-party:10.0.0
 
-All of these must have a `policy.*.audit-as-crates-io` entry
+  × There are some issues with your policy.audit-as-crates-io entries
+
+Error: 
+  × Some first-party packages match published crates.io versions
+  │   first-party:10.0.0
+  help: Add a `policy.*.audit-as-crates-io` entry for them