diff --git a/Cargo.lock b/Cargo.lock
index 703f0e5b8af9..a4c8911c12a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1123,7 +1123,7 @@ checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
 
 [[package]]
 name = "paths"
-version = "0.0.0"
+version = "0.1.0"
 
 [[package]]
 name = "percent-encoding"
diff --git a/crates/flycheck/Cargo.toml b/crates/flycheck/Cargo.toml
index d3d180ece512..42e86b8dd73a 100644
--- a/crates/flycheck/Cargo.toml
+++ b/crates/flycheck/Cargo.toml
@@ -19,4 +19,4 @@ jod-thread = "0.1.2"
 
 toolchain = { path = "../toolchain", version = "0.0.0" }
 stdx = { path = "../stdx", version = "0.0.0" }
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
diff --git a/crates/proc-macro-api/Cargo.toml b/crates/proc-macro-api/Cargo.toml
index 85a1c13fe7d4..30e73211c555 100644
--- a/crates/proc-macro-api/Cargo.toml
+++ b/crates/proc-macro-api/Cargo.toml
@@ -23,7 +23,7 @@ tracing = "0.1.35"
 memmap2 = "0.5.4"
 snap = "1.0.5"
 
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
 tt = { path = "../tt", version = "0.0.0" }
 stdx = { path = "../stdx", version = "0.0.0" }
 profile = { path = "../profile", version = "0.0.0" }
diff --git a/crates/proc-macro-srv/Cargo.toml b/crates/proc-macro-srv/Cargo.toml
index 5746eac0b379..a3a8a4ec55db 100644
--- a/crates/proc-macro-srv/Cargo.toml
+++ b/crates/proc-macro-srv/Cargo.toml
@@ -22,7 +22,7 @@ memmap2 = "0.5.4"
 
 tt = { path = "../tt", version = "0.0.0" }
 mbe = { path = "../mbe", version = "0.0.0" }
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
 proc-macro-api = { path = "../proc-macro-api", version = "0.0.0" }
 crossbeam = "0.8.1"
 
diff --git a/crates/proc-macro-srv/src/tests/mod.rs b/crates/proc-macro-srv/src/tests/mod.rs
index 07222907f088..8941f3a88d32 100644
--- a/crates/proc-macro-srv/src/tests/mod.rs
+++ b/crates/proc-macro-srv/src/tests/mod.rs
@@ -160,7 +160,7 @@ fn list_test_macros() {
 
 #[test]
 fn test_version_check() {
-    let path = AbsPathBuf::assert(fixtures::proc_macro_test_dylib_path());
+    let path = AbsPathBuf::try_from(fixtures::proc_macro_test_dylib_path()).unwrap();
     let info = proc_macro_api::read_dylib_info(&path).unwrap();
     assert!(info.version.1 >= 50);
 }
diff --git a/crates/project-model/Cargo.toml b/crates/project-model/Cargo.toml
index bc75d6faa383..8eb51c70827c 100644
--- a/crates/project-model/Cargo.toml
+++ b/crates/project-model/Cargo.toml
@@ -23,6 +23,6 @@ la-arena = { version = "0.3.0", path = "../../lib/la-arena" }
 cfg = { path = "../cfg", version = "0.0.0" }
 base-db = { path = "../base-db", version = "0.0.0" }
 toolchain = { path = "../toolchain", version = "0.0.0" }
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
 stdx = { path = "../stdx", version = "0.0.0" }
 profile = { path = "../profile", version = "0.0.0" }
diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs
index ee7f8339a76a..ad55fe365bca 100644
--- a/crates/project-model/src/build_scripts.rs
+++ b/crates/project-model/src/build_scripts.rs
@@ -147,7 +147,8 @@ impl WorkspaceBuildScripts {
                         let out_dir = message.out_dir.into_os_string();
                         if !out_dir.is_empty() {
                             let data = outputs[package].get_or_insert_with(Default::default);
-                            data.out_dir = Some(AbsPathBuf::assert(PathBuf::from(out_dir)));
+                            data.out_dir =
+                                Some(AbsPathBuf::try_from(PathBuf::from(out_dir)).unwrap());
                             data.cfgs = cfgs;
                         }
                         if !message.env.is_empty() {
@@ -168,7 +169,8 @@ impl WorkspaceBuildScripts {
                             if let Some(filename) =
                                 message.filenames.iter().find(|name| is_dylib(name))
                             {
-                                let filename = AbsPathBuf::assert(PathBuf::from(&filename));
+                                let filename =
+                                    AbsPathBuf::try_from(PathBuf::from(&filename)).unwrap();
                                 outputs[package]
                                     .get_or_insert_with(Default::default)
                                     .proc_macro_dylib_path = Some(filename);
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index 597880c2ca21..1591cbcac353 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -336,7 +336,10 @@ impl CargoWorkspace {
                 id: id.repr.clone(),
                 name: name.clone(),
                 version: version.clone(),
-                manifest: AbsPathBuf::assert(PathBuf::from(&manifest_path)).try_into().unwrap(),
+                manifest: AbsPathBuf::try_from(PathBuf::from(&manifest_path))
+                    .unwrap()
+                    .try_into()
+                    .unwrap(),
                 targets: Vec::new(),
                 is_local,
                 is_member,
@@ -354,7 +357,7 @@ impl CargoWorkspace {
                 let tgt = targets.alloc(TargetData {
                     package: pkg,
                     name: meta_tgt.name.clone(),
-                    root: AbsPathBuf::assert(PathBuf::from(&meta_tgt.src_path)),
+                    root: AbsPathBuf::try_from(PathBuf::from(&meta_tgt.src_path)).unwrap(),
                     kind: TargetKind::new(meta_tgt.kind.as_slice()),
                     is_proc_macro,
                     required_features: meta_tgt.required_features.clone(),
@@ -397,7 +400,7 @@ impl CargoWorkspace {
         }
 
         let workspace_root =
-            AbsPathBuf::assert(PathBuf::from(meta.workspace_root.into_os_string()));
+            AbsPathBuf::try_from(PathBuf::from(meta.workspace_root.into_os_string())).unwrap();
 
         CargoWorkspace { packages, targets, workspace_root }
     }
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index e3f83084ac8a..c82b4f42a958 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -125,7 +125,7 @@ impl ProjectManifest {
                 .filter_map(Result::ok)
                 .map(|it| it.path().join("Cargo.toml"))
                 .filter(|it| it.exists())
-                .map(AbsPathBuf::assert)
+                .map(|x| AbsPathBuf::try_from(x).unwrap())
                 .filter_map(|it| it.try_into().ok())
                 .collect()
         }
diff --git a/crates/project-model/src/sysroot.rs b/crates/project-model/src/sysroot.rs
index 362bb0f5e79c..86df86c41150 100644
--- a/crates/project-model/src/sysroot.rs
+++ b/crates/project-model/src/sysroot.rs
@@ -149,7 +149,7 @@ fn discover_sysroot_dir(current_dir: &AbsPath) -> Result<AbsPathBuf> {
     rustc.current_dir(current_dir).args(&["--print", "sysroot"]);
     tracing::debug!("Discovering sysroot by {:?}", rustc);
     let stdout = utf8_stdout(rustc)?;
-    Ok(AbsPathBuf::assert(PathBuf::from(stdout)))
+    Ok(AbsPathBuf::try_from(PathBuf::from(stdout)).unwrap())
 }
 
 fn discover_sysroot_src_dir(
@@ -157,8 +157,9 @@ fn discover_sysroot_src_dir(
     current_dir: &AbsPath,
 ) -> Result<AbsPathBuf> {
     if let Ok(path) = env::var("RUST_SRC_PATH") {
-        let path = AbsPathBuf::try_from(path.as_str())
-            .map_err(|path| format_err!("RUST_SRC_PATH must be absolute: {}", path.display()))?;
+        let path = AbsPathBuf::try_from(path.as_str()).map_err(|path| {
+            format_err!("RUST_SRC_PATH must be absolute: {}", path.into_inner().display())
+        })?;
         let core = path.join("core");
         if fs::metadata(&core).is_ok() {
             tracing::debug!("Discovered sysroot by RUST_SRC_PATH: {}", path.display());
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index e304a59c0180..3945e9cbff94 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -77,7 +77,7 @@ fn get_fake_sysroot() -> Sysroot {
     let sysroot_path = get_test_path("fake-sysroot");
     // there's no `libexec/` directory with a `proc-macro-srv` binary in that
     // fake sysroot, so we give them both the same path:
-    let sysroot_dir = AbsPathBuf::assert(sysroot_path);
+    let sysroot_dir = AbsPathBuf::try_from(sysroot_path).unwrap();
     let sysroot_src_dir = sysroot_dir.clone();
     Sysroot::load(sysroot_dir, sysroot_src_dir).unwrap()
 }
@@ -86,7 +86,7 @@ fn rooted_project_json(data: ProjectJsonData) -> ProjectJson {
     let mut root = "$ROOT$".to_string();
     replace_root(&mut root, true);
     let path = Path::new(&root);
-    let base = AbsPath::assert(path);
+    let base = <&AbsPath>::try_from(path).unwrap();
     ProjectJson::new(base, data)
 }
 
diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs
index e9de23cb395d..8fe54e2d93c6 100644
--- a/crates/rust-analyzer/src/bin/main.rs
+++ b/crates/rust-analyzer/src/bin/main.rs
@@ -168,7 +168,7 @@ fn run_server() -> Result<()> {
         Some(it) => it,
         None => {
             let cwd = env::current_dir()?;
-            AbsPathBuf::assert(cwd)
+            AbsPathBuf::try_from(cwd).unwrap()
         }
     };
 
diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs
index f52e1e751278..8ac974dc9e1d 100644
--- a/crates/rust-analyzer/src/cli/analysis_stats.rs
+++ b/crates/rust-analyzer/src/cli/analysis_stats.rs
@@ -65,7 +65,7 @@ impl flags::AnalysisStats {
 
         let mut db_load_sw = self.stop_watch();
 
-        let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path));
+        let path = AbsPathBuf::try_from(env::current_dir()?.join(&self.path)).unwrap();
         let manifest = ProjectManifest::discover_single(&path)?;
 
         let mut workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
diff --git a/crates/rust-analyzer/src/cli/load_cargo.rs b/crates/rust-analyzer/src/cli/load_cargo.rs
index 5d1c013c3275..35876aaa4c30 100644
--- a/crates/rust-analyzer/src/cli/load_cargo.rs
+++ b/crates/rust-analyzer/src/cli/load_cargo.rs
@@ -29,7 +29,7 @@ pub fn load_workspace_at(
     load_config: &LoadCargoConfig,
     progress: &dyn Fn(String),
 ) -> Result<(AnalysisHost, vfs::Vfs, Option<ProcMacroServer>)> {
-    let root = AbsPathBuf::assert(std::env::current_dir()?.join(root));
+    let root = AbsPathBuf::try_from(std::env::current_dir()?.join(root)).unwrap();
     let root = ProjectManifest::discover_single(&root)?;
     let mut workspace = ProjectWorkspace::load(root, cargo_config, progress)?;
 
@@ -59,7 +59,7 @@ pub fn load_workspace(
     };
 
     let proc_macro_client = if load_config.with_proc_macro {
-        let path = AbsPathBuf::assert(std::env::current_exe()?);
+        let path = AbsPathBuf::try_from(std::env::current_exe()?).unwrap();
         Ok(ProcMacroServer::spawn(path, &["proc-macro"]).unwrap())
     } else {
         Err("proc macro server not started".to_owned())
diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs
index 491c55a04f8c..41e19ae72736 100644
--- a/crates/rust-analyzer/src/cli/lsif.rs
+++ b/crates/rust-analyzer/src/cli/lsif.rs
@@ -294,7 +294,7 @@ impl flags::Lsif {
             with_proc_macro: true,
             prefill_caches: false,
         };
-        let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path));
+        let path = AbsPathBuf::try_from(env::current_dir()?.join(&self.path)).unwrap();
         let manifest = ProjectManifest::discover_single(&path)?;
 
         let workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index ac0fdf85a774..d3a18c071c5a 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -609,7 +609,7 @@ impl Config {
         self.detached_files =
             get_field::<Vec<PathBuf>>(&mut json, &mut errors, "detachedFiles", None, "[]")
                 .into_iter()
-                .map(AbsPathBuf::assert)
+                .map(|x| AbsPathBuf::try_from(x).unwrap())
                 .collect();
         patch_old_style::patch_json_for_outdated_configs(&mut json);
         self.data = ConfigData::from_json(json, &mut errors);
@@ -902,7 +902,7 @@ impl Config {
         }
         let path = match &self.data.procMacro_server {
             Some(it) => self.root_path.join(it),
-            None => AbsPathBuf::assert(std::env::current_exe().ok()?),
+            None => AbsPathBuf::try_from(std::env::current_exe().ok()?).unwrap(),
         };
         Some((path, vec!["proc-macro".into()]))
     }
diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs
index 47cdd8dfc75d..eeda00d4be14 100644
--- a/crates/rust-analyzer/src/integrated_benchmarks.rs
+++ b/crates/rust-analyzer/src/integrated_benchmarks.rs
@@ -47,7 +47,7 @@ fn integrated_highlighting_benchmark() {
 
     let file_id = {
         let file = workspace_to_load.join(file);
-        let path = VfsPath::from(AbsPathBuf::assert(file));
+        let path = VfsPath::from(AbsPathBuf::try_from(file).unwrap());
         vfs.file_id(&path).unwrap_or_else(|| panic!("can't find virtual file for {}", path))
     };
 
@@ -101,7 +101,7 @@ fn integrated_completion_benchmark() {
 
     let file_id = {
         let file = workspace_to_load.join(file);
-        let path = VfsPath::from(AbsPathBuf::assert(file));
+        let path = VfsPath::from(AbsPathBuf::try_from(file).unwrap());
         vfs.file_id(&path).unwrap_or_else(|| panic!("can't find virtual file for {}", path))
     };
 
diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs
index 4fa88c3c6da1..0642dec81960 100644
--- a/crates/rust-analyzer/tests/slow-tests/support.rs
+++ b/crates/rust-analyzer/tests/slow-tests/support.rs
@@ -89,7 +89,7 @@ impl<'a> Project<'a> {
             fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
         }
 
-        let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf());
+        let tmp_dir_path = AbsPathBuf::try_from(tmp_dir.path().to_path_buf()).unwrap();
         let mut roots =
             self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::<Vec<_>>();
         if roots.is_empty() {
diff --git a/crates/vfs-notify/Cargo.toml b/crates/vfs-notify/Cargo.toml
index 9ee4415dcada..212f90555047 100644
--- a/crates/vfs-notify/Cargo.toml
+++ b/crates/vfs-notify/Cargo.toml
@@ -17,4 +17,4 @@ crossbeam-channel = "0.5.5"
 notify = "=5.0.0-pre.15"
 
 vfs = { path = "../vfs", version = "0.0.0" }
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs
index 4d33a9afb963..1e9e91652736 100644
--- a/crates/vfs-notify/src/lib.rs
+++ b/crates/vfs-notify/src/lib.rs
@@ -183,7 +183,7 @@ impl NotifyActor {
                             if !entry.file_type().is_dir() {
                                 return true;
                             }
-                            let path = AbsPath::assert(entry.path());
+                            let path = <&AbsPath>::try_from(entry.path()).unwrap();
                             root == path
                                 || dirs.exclude.iter().chain(&dirs.include).all(|it| it != path)
                         });
@@ -191,7 +191,7 @@ impl NotifyActor {
                     let files = walkdir.filter_map(|it| it.ok()).filter_map(|entry| {
                         let is_dir = entry.file_type().is_dir();
                         let is_file = entry.file_type().is_file();
-                        let abs_path = AbsPathBuf::assert(entry.into_path());
+                        let abs_path = AbsPathBuf::try_from(entry.into_path()).unwrap();
                         if is_dir && watch {
                             self.watch(abs_path.clone());
                         }
diff --git a/crates/vfs/Cargo.toml b/crates/vfs/Cargo.toml
index c6377348784a..f92c64ccecd1 100644
--- a/crates/vfs/Cargo.toml
+++ b/crates/vfs/Cargo.toml
@@ -13,5 +13,5 @@ doctest = false
 rustc-hash = "1.1.0"
 fst = "0.4.7"
 
-paths = { path = "../paths", version = "0.0.0" }
+paths = { path = "../../lib/paths", version = "0.1.0" }
 indexmap = "1.9.1"
diff --git a/crates/vfs/src/vfs_path.rs b/crates/vfs/src/vfs_path.rs
index 668c7320d4ec..189d2f4d79fb 100644
--- a/crates/vfs/src/vfs_path.rs
+++ b/crates/vfs/src/vfs_path.rs
@@ -28,7 +28,7 @@ impl VfsPath {
     /// Create a path from string. Input should be a string representation of
     /// an absolute path inside filesystem
     pub fn new_real_path(path: String) -> VfsPath {
-        VfsPath::from(AbsPathBuf::assert(path.into()))
+        VfsPath::from(AbsPathBuf::try_from(path).unwrap())
     }
 
     /// Returns the `AbsPath` representation of `self` if `self` is on the file system.
diff --git a/lib/README.md b/lib/README.md
index 6b2eeac2c0d7..8c790456fd54 100644
--- a/lib/README.md
+++ b/lib/README.md
@@ -1,2 +1,3 @@
 Crates in this directory are published to crates.io and obey semver.
-They *could* live in a separate repo, but we want to experiment with a monorepo setup.
+
+They _could_ live in a separate repo, but we want to experiment with a monorepo setup.
diff --git a/crates/paths/Cargo.toml b/lib/paths/Cargo.toml
similarity index 80%
rename from crates/paths/Cargo.toml
rename to lib/paths/Cargo.toml
index 5e83de7d994e..2b3843392566 100644
--- a/crates/paths/Cargo.toml
+++ b/lib/paths/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "paths"
-version = "0.0.0"
-description = "TBD"
+version = "0.1.0"
+description = "Wrapper types for relative and absolute paths."
 license = "MIT OR Apache-2.0"
 edition = "2021"
 rust-version = "1.57"
diff --git a/crates/paths/src/lib.rs b/lib/paths/src/lib.rs
similarity index 58%
rename from crates/paths/src/lib.rs
rename to lib/paths/src/lib.rs
index 025093f4a94a..e501f63d4676 100644
--- a/crates/paths/src/lib.rs
+++ b/lib/paths/src/lib.rs
@@ -6,10 +6,15 @@
 use std::{
     borrow::Borrow,
     ffi::OsStr,
-    ops,
+    fmt, io, ops,
     path::{Component, Path, PathBuf},
 };
 
+/// Returns an [`AbsPathBuf`] of the current directory joined with the `path`.
+pub fn to_abs_path(path: &Path) -> io::Result<AbsPathBuf> {
+    Ok(std::env::current_dir()?.join(path).try_into().unwrap())
+}
+
 /// Wrapper around an absolute [`PathBuf`].
 #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
 pub struct AbsPathBuf(PathBuf);
@@ -46,18 +51,45 @@ impl Borrow<AbsPath> for AbsPathBuf {
 }
 
 impl TryFrom<PathBuf> for AbsPathBuf {
-    type Error = PathBuf;
-    fn try_from(path_buf: PathBuf) -> Result<AbsPathBuf, PathBuf> {
-        if !path_buf.is_absolute() {
-            return Err(path_buf);
+    type Error = NotAbsPathBuf;
+    fn try_from(path_buf: PathBuf) -> Result<AbsPathBuf, NotAbsPathBuf> {
+        if path_buf.is_absolute() {
+            Ok(AbsPathBuf(path_buf))
+        } else {
+            Err(NotAbsPathBuf(path_buf))
         }
-        Ok(AbsPathBuf(path_buf))
     }
 }
 
+/// The error returned by `impl TryFrom<PathBuf> for AbsPathBuf` and others.
+#[derive(Debug)]
+pub struct NotAbsPathBuf(PathBuf);
+
+impl NotAbsPathBuf {
+    /// Returns the `PathBuf` that could not be converted.
+    pub fn into_inner(self) -> PathBuf {
+        self.0
+    }
+}
+
+impl fmt::Display for NotAbsPathBuf {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} is not an absolute path", self.0.display())
+    }
+}
+
+impl std::error::Error for NotAbsPathBuf {}
+
 impl TryFrom<&str> for AbsPathBuf {
-    type Error = PathBuf;
-    fn try_from(path: &str) -> Result<AbsPathBuf, PathBuf> {
+    type Error = NotAbsPathBuf;
+    fn try_from(path: &str) -> Result<AbsPathBuf, NotAbsPathBuf> {
+        AbsPathBuf::try_from(PathBuf::from(path))
+    }
+}
+
+impl TryFrom<String> for AbsPathBuf {
+    type Error = NotAbsPathBuf;
+    fn try_from(path: String) -> Result<AbsPathBuf, NotAbsPathBuf> {
         AbsPathBuf::try_from(PathBuf::from(path))
     }
 }
@@ -69,24 +101,15 @@ impl PartialEq<AbsPath> for AbsPathBuf {
 }
 
 impl AbsPathBuf {
-    /// Wrap the given absolute path in `AbsPathBuf`
+    /// Coerces to an [`AbsPath`] slice.
     ///
-    /// # Panics
-    ///
-    /// Panics if `path` is not absolute.
-    pub fn assert(path: PathBuf) -> AbsPathBuf {
-        AbsPathBuf::try_from(path)
-            .unwrap_or_else(|path| panic!("expected absolute path, got {}", path.display()))
-    }
-
-    /// Coerces to an `AbsPath` slice.
-    ///
-    /// Equivalent of [`PathBuf::as_path`] for `AbsPathBuf`.
+    /// Equivalent of [`PathBuf::as_path`].
     pub fn as_path(&self) -> &AbsPath {
-        AbsPath::assert(self.0.as_path())
+        // SAFETY: `AbsPathBuf` always contains an absolute path
+        unsafe { AbsPath::new_unchecked(self.0.as_path()) }
     }
 
-    /// Equivalent of [`PathBuf::pop`] for `AbsPathBuf`.
+    /// Equivalent of [`PathBuf::pop`].
     ///
     /// Note that this won't remove the root component, so `self` will still be
     /// absolute.
@@ -107,37 +130,54 @@ impl AsRef<Path> for AbsPath {
 }
 
 impl<'a> TryFrom<&'a Path> for &'a AbsPath {
-    type Error = &'a Path;
-    fn try_from(path: &'a Path) -> Result<&'a AbsPath, &'a Path> {
-        if !path.is_absolute() {
-            return Err(path);
+    type Error = NotAbsPath<'a>;
+    fn try_from(path: &'a Path) -> Result<&'a AbsPath, NotAbsPath<'a>> {
+        if path.is_absolute() {
+            // SAFETY: just checked is absolute
+            Ok(unsafe { AbsPath::new_unchecked(path) })
+        } else {
+            Err(NotAbsPath(path))
         }
-        Ok(AbsPath::assert(path))
     }
 }
 
+/// The error returned by `impl TryFrom<&Path> for &AbsPath` and others.
+#[derive(Debug)]
+pub struct NotAbsPath<'a>(&'a Path);
+
+impl<'a> NotAbsPath<'a> {
+    /// Returns the `Path` that could not be converted.
+    pub fn into_inner(self) -> &'a Path {
+        self.0
+    }
+}
+
+impl<'a> fmt::Display for NotAbsPath<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} is not an absolute path", self.0.display())
+    }
+}
+
+impl<'a> std::error::Error for NotAbsPath<'a> {}
+
 impl AbsPath {
-    /// Wrap the given absolute path in `AbsPath`
-    ///
-    /// # Panics
-    ///
-    /// Panics if `path` is not absolute.
-    pub fn assert(path: &Path) -> &AbsPath {
-        assert!(path.is_absolute());
-        unsafe { &*(path as *const Path as *const AbsPath) }
+    unsafe fn new_unchecked(path: &Path) -> &AbsPath {
+        &*(path as *const Path as *const AbsPath)
     }
 
     /// Equivalent of [`Path::parent`] for `AbsPath`.
     pub fn parent(&self) -> Option<&AbsPath> {
-        self.0.parent().map(AbsPath::assert)
+        // SAFETY: the parent of an absolute path will be absolute
+        self.0.parent().map(|x| unsafe { AbsPath::new_unchecked(x) })
     }
 
     /// Equivalent of [`Path::join`] for `AbsPath`.
-    pub fn join(&self, path: impl AsRef<Path>) -> AbsPathBuf {
+    pub fn join<P: AsRef<Path>>(&self, path: P) -> AbsPathBuf {
         self.as_ref().join(path).try_into().unwrap()
     }
 
     /// Normalize the given path:
+    ///
     /// - Removes repeated separators: `/a//b` becomes `/a/b`
     /// - Removes occurrences of `.` and resolves `..`.
     /// - Removes trailing slashes: `/a/b/` becomes `/a/b`.
@@ -150,7 +190,7 @@ impl AbsPath {
     /// assert_eq!(normalized, AbsPathBuf::assert("/b/c".into()));
     /// ```
     pub fn normalize(&self) -> AbsPathBuf {
-        AbsPathBuf(normalize_path(&self.0))
+        AbsPathBuf::try_from(normalize_path(&self.0)).unwrap()
     }
 
     /// Equivalent of [`Path::to_path_buf`] for `AbsPath`.
@@ -162,11 +202,19 @@ impl AbsPath {
     ///
     /// Returns a relative path.
     pub fn strip_prefix(&self, base: &AbsPath) -> Option<&RelPath> {
-        self.0.strip_prefix(base).ok().map(RelPath::new_unchecked)
+        self.0.strip_prefix(base).ok().map(|x| {
+            // SAFETY: if the prefix was stripped, the prefix must have been
+            // absolute
+            unsafe { RelPath::new_unchecked(x) }
+        })
     }
+
+    /// Returns whether `self` starts with `base`.
     pub fn starts_with(&self, base: &AbsPath) -> bool {
         self.0.starts_with(&base.0)
     }
+
+    /// Returns whether `self` starts with `suffix`.
     pub fn ends_with(&self, suffix: &RelPath) -> bool {
         self.0.ends_with(&suffix.0)
     }
@@ -181,25 +229,30 @@ impl AbsPath {
     // For `AbsPath`, we want to make sure that this is a POD type, and that all
     // IO goes via `fs`. That way, it becomes easier to mock IO when we need it.
 
+    /// Delegate for [`Path::file_name`].
     pub fn file_name(&self) -> Option<&OsStr> {
         self.0.file_name()
     }
+
+    /// Delegate for [`Path::extension`].
     pub fn extension(&self) -> Option<&OsStr> {
         self.0.extension()
     }
+
+    /// Delegate for [`Path::file_stem`].
     pub fn file_stem(&self) -> Option<&OsStr> {
         self.0.file_stem()
     }
+
+    /// Delegate for [`Path::as_os_str`].
     pub fn as_os_str(&self) -> &OsStr {
         self.0.as_os_str()
     }
+
+    /// Delegate for [`Path::display`].
     pub fn display(&self) -> std::path::Display<'_> {
         self.0.display()
     }
-    #[deprecated(note = "use std::fs::metadata().is_ok() instead")]
-    pub fn exists(&self) -> bool {
-        self.0.exists()
-    }
     // endregion:delegate-methods
 }
 
@@ -227,18 +280,38 @@ impl AsRef<Path> for RelPathBuf {
 }
 
 impl TryFrom<PathBuf> for RelPathBuf {
-    type Error = PathBuf;
-    fn try_from(path_buf: PathBuf) -> Result<RelPathBuf, PathBuf> {
-        if !path_buf.is_relative() {
-            return Err(path_buf);
+    type Error = NotRelPathBuf;
+    fn try_from(path_buf: PathBuf) -> Result<RelPathBuf, NotRelPathBuf> {
+        if path_buf.is_relative() {
+            Ok(RelPathBuf(path_buf))
+        } else {
+            Err(NotRelPathBuf(path_buf))
         }
-        Ok(RelPathBuf(path_buf))
     }
 }
 
+/// The error returned by `impl TryFrom<PathBuf> for RelPathBuf` and others.
+#[derive(Debug)]
+pub struct NotRelPathBuf(PathBuf);
+
+impl NotRelPathBuf {
+    /// Returns the `PathBuf` that could not be converted.
+    pub fn into_inner(self) -> PathBuf {
+        self.0
+    }
+}
+
+impl fmt::Display for NotRelPathBuf {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} is not an relative path", self.0.display())
+    }
+}
+
+impl std::error::Error for NotRelPathBuf {}
+
 impl TryFrom<&str> for RelPathBuf {
-    type Error = PathBuf;
-    fn try_from(path: &str) -> Result<RelPathBuf, PathBuf> {
+    type Error = NotRelPathBuf;
+    fn try_from(path: &str) -> Result<RelPathBuf, NotRelPathBuf> {
         RelPathBuf::try_from(PathBuf::from(path))
     }
 }
@@ -248,7 +321,8 @@ impl RelPathBuf {
     ///
     /// Equivalent of [`PathBuf::as_path`] for `RelPathBuf`.
     pub fn as_path(&self) -> &RelPath {
-        RelPath::new_unchecked(self.0.as_path())
+        // SAFETY: `RelPathBuf` always contains relative paths
+        unsafe { RelPath::new_unchecked(self.0.as_path()) }
     }
 }
 
@@ -265,8 +339,8 @@ impl AsRef<Path> for RelPath {
 
 impl RelPath {
     /// Creates a new `RelPath` from `path`, without checking if it is relative.
-    pub fn new_unchecked(path: &Path) -> &RelPath {
-        unsafe { &*(path as *const Path as *const RelPath) }
+    unsafe fn new_unchecked(path: &Path) -> &RelPath {
+        &*(path as *const Path as *const RelPath)
     }
 }