Repository: getsentry/symbolic
Pull request #264 (2020-09-17 11:00:44)

ref: Replace failure with std::error::Error
Ref https://github.com/getsentry/symbolic/issues/259

jan-auer: This is essentially ready for review, thanks a bunch to @Swatinem for wrapping this up. 

I'll also do another pass through all changes, so please don't merge just yet.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11bee229..203b5913 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## Unreleased
+
+**Breaking Changes**:
+
+- Usage of `failure` was removed, and all Error types were changed to only implement `std::error::Error` and related traits.
+- `symbolic-proguard` was removed in favor of the `proguard` crate. Proguard is still supported via `symbolic-cabi` and the python API however.
+
 ## 7.5.0
 
 **Changes**:
diff --git a/examples/addr2line/Cargo.toml b/examples/addr2line/Cargo.toml
index d11e5a52..232c13a8 100644
--- a/examples/addr2line/Cargo.toml
+++ b/examples/addr2line/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow = "1.0.32"
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic", features = ["demangle"] }
diff --git a/examples/addr2line/src/main.rs b/examples/addr2line/src/main.rs
index ebd5c382..51f1c6ae 100644
--- a/examples/addr2line/src/main.rs
+++ b/examples/addr2line/src/main.rs
@@ -1,20 +1,12 @@
 use std::borrow::Borrow;
 
+use anyhow::{Context, Result};
 use clap::{clap_app, ArgMatches};
-use failure::{Error, ResultExt};
 
 use symbolic::common::{ByteView, Name};
 use symbolic::debuginfo::{Function, Object};
 use symbolic::demangle::Demangle;
 
-fn print_error(error: &Error) {
-    eprintln!("Error: {}", error);
-
-    for cause in error.iter_causes() {
-        eprintln!("   caused by {}", cause);
-    }
-}
-
 fn print_name<'a, N: Borrow<Name<'a>>>(name: Option<N>, matches: &ArgMatches<'_>) {
     match name.as_ref().map(Borrow::borrow) {
         None => print!("??"),
@@ -36,7 +28,7 @@ fn print_range(start: u64, len: Option<u64>, matches: &ArgMatches<'_>) {
     }
 }
 
-fn resolve(function: &Function<'_>, addr: u64, matches: &ArgMatches<'_>) -> Result<bool, Error> {
+fn resolve(function: &Function<'_>, addr: u64, matches: &ArgMatches<'_>) -> Result<bool> {
     if function.address > addr || function.address + function.size <= addr {
         return Ok(false);
     }
@@ -76,7 +68,7 @@ fn resolve(function: &Function<'_>, addr: u64, matches: &ArgMatches<'_>) -> Resu
     Ok(false)
 }
 
-fn execute(matches: &ArgMatches<'_>) -> Result<(), Error> {
+fn execute(matches: &ArgMatches<'_>) -> Result<()> {
     let path = matches.value_of("path").unwrap_or("a.out");
     let view = ByteView::open(path).context("failed to open file")?;
     let object = Object::parse(&view).context("failed to parse file")?;
@@ -127,6 +119,6 @@ fn main() {
 
     match execute(&matches) {
         Ok(()) => (),
-        Err(e) => print_error(&e),
+        Err(e) => eprintln!("{:?}", e),
     };
 }
diff --git a/examples/dump_cfi/Cargo.toml b/examples/dump_cfi/Cargo.toml
index e8689fe7..9af56d81 100644
--- a/examples/dump_cfi/Cargo.toml
+++ b/examples/dump_cfi/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow = "1.0.32"
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic", features = ["minidump"] }
diff --git a/examples/dump_cfi/src/main.rs b/examples/dump_cfi/src/main.rs
index a4285b20..bff2220d 100644
--- a/examples/dump_cfi/src/main.rs
+++ b/examples/dump_cfi/src/main.rs
@@ -1,21 +1,13 @@
 use std::path::Path;
 
+use anyhow::Result;
 use clap::{App, Arg, ArgMatches};
-use failure::Error;
 
 use symbolic::common::{ByteView, DSymPathExt};
 use symbolic::debuginfo::Object;
 use symbolic::minidump::cfi::AsciiCfiWriter;
 
-fn print_error(error: &Error) {
-    eprintln!("Error: {:#?}", error);
-
-    for cause in error.iter_causes() {
-        eprintln!("   caused by {}", cause);
-    }
-}
-
-fn dump_cfi<P: AsRef<Path>>(path: P) -> Result<(), Error> {
+fn dump_cfi<P: AsRef<Path>>(path: P) -> Result<()> {
     let path = path.as_ref();
 
     let dsym_path = path.resolve_dsym();
@@ -36,7 +28,7 @@ fn dump_cfi<P: AsRef<Path>>(path: P) -> Result<(), Error> {
     Ok(())
 }
 
-fn execute(matches: &ArgMatches<'_>) -> Result<(), Error> {
+fn execute(matches: &ArgMatches<'_>) -> Result<()> {
     let path = matches.value_of("path").unwrap();
     dump_cfi(path)
 }
@@ -56,6 +48,6 @@ fn main() {
 
     match execute(&matches) {
         Ok(()) => (),
-        Err(e) => print_error(&e),
+        Err(e) => eprintln!("{:?}", e),
     };
 }
diff --git a/examples/dump_sources/Cargo.toml b/examples/dump_sources/Cargo.toml
index 047ac75b..5d32f84b 100644
--- a/examples/dump_sources/Cargo.toml
+++ b/examples/dump_sources/Cargo.toml
@@ -8,5 +8,4 @@ edition = "2018"
 
 [dependencies]
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic" }
diff --git a/examples/dump_sources/src/main.rs b/examples/dump_sources/src/main.rs
index af9009e3..a2ec6a34 100644
--- a/examples/dump_sources/src/main.rs
+++ b/examples/dump_sources/src/main.rs
@@ -1,21 +1,21 @@
 use std::path::Path;
 
 use clap::{App, Arg, ArgMatches};
-use failure::Error;
 
 use symbolic::common::{ByteView, DSymPathExt};
 use symbolic::debuginfo::sourcebundle::SourceBundleWriter;
 use symbolic::debuginfo::Archive;
 
-fn print_error(error: &Error) {
+fn print_error(mut error: &dyn std::error::Error) {
     println!("Error: {}", error);
 
-    for cause in error.iter_causes() {
-        println!("   caused by {}", cause);
+    while let Some(source) = error.source() {
+        println!("   caused by {}", source);
+        error = source;
     }
 }
 
-fn write_object_sources(path: &Path, output_path: &Path) -> Result<(), Error> {
+fn write_object_sources(path: &Path, output_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
     println!("Inspecting {}", path.display());
 
     let dsym_path = path.resolve_dsym();
@@ -34,7 +34,7 @@ fn write_object_sources(path: &Path, output_path: &Path) -> Result<(), Error> {
             }
             Err(e) => {
                 print!(" - ");
-                print_error(&e.into());
+                print_error(&e);
                 continue;
             }
         }
@@ -43,18 +43,15 @@ fn write_object_sources(path: &Path, output_path: &Path) -> Result<(), Error> {
     Ok(())
 }
 
-fn execute(matches: &ArgMatches<'_>) -> Result<(), Error> {
+fn execute(matches: &ArgMatches<'_>) {
     let output_path = Path::new(matches.value_of("output").unwrap());
     for path in matches.values_of("paths").unwrap_or_default() {
-        match write_object_sources(Path::new(&path), &output_path) {
-            Ok(()) => (),
-            Err(e) => print_error(&e),
+        if let Err(e) = write_object_sources(Path::new(&path), &output_path) {
+            print_error(e.as_ref());
         }
 
         println!();
     }
-
-    Ok(())
 }
 
 fn main() {
@@ -79,8 +76,5 @@ fn main() {
         )
         .get_matches();
 
-    match execute(&matches) {
-        Ok(()) => (),
-        Err(e) => print_error(&e),
-    };
+    execute(&matches);
 }
diff --git a/examples/minidump_stackwalk/Cargo.toml b/examples/minidump_stackwalk/Cargo.toml
index 1fd89e0e..1a2c5db5 100644
--- a/examples/minidump_stackwalk/Cargo.toml
+++ b/examples/minidump_stackwalk/Cargo.toml
@@ -8,6 +8,5 @@ edition = "2018"
 
 [dependencies]
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic", features = ["minidump", "symcache", "demangle"] }
 walkdir = "2.3.1"
diff --git a/examples/minidump_stackwalk/src/main.rs b/examples/minidump_stackwalk/src/main.rs
index 369eaaab..ce52784a 100644
--- a/examples/minidump_stackwalk/src/main.rs
+++ b/examples/minidump_stackwalk/src/main.rs
@@ -3,7 +3,6 @@ use std::io::Cursor;
 use std::path::Path;
 
 use clap::{App, Arg, ArgMatches};
-use failure::Error;
 use walkdir::WalkDir;
 
 use symbolic::common::{Arch, ByteView, InstructionInfo, SelfCell};
@@ -11,9 +10,10 @@ use symbolic::debuginfo::{Archive, FileFormat, Object};
 use symbolic::demangle::Demangle;
 use symbolic::minidump::cfi::CfiCache;
 use symbolic::minidump::processor::{CodeModuleId, FrameInfoMap, ProcessState, StackFrame};
-use symbolic::symcache::{LineInfo, SymCache, SymCacheWriter};
+use symbolic::symcache::{LineInfo, SymCache, SymCacheError, SymCacheWriter};
 
 type SymCaches<'a> = BTreeMap<CodeModuleId, SelfCell<ByteView<'a>, SymCache<'a>>>;
+type Error = Box<dyn std::error::Error>;
 
 fn collect_referenced_objects<P, F, T>(
     path: P,
@@ -124,7 +124,7 @@ fn symbolize<'a>(
     frame: &StackFrame,
     arch: Arch,
     crashing: bool,
-) -> Result<Option<Vec<LineInfo<'a>>>, Error> {
+) -> Result<Option<Vec<LineInfo<'a>>>, SymCacheError> {
     let module = match frame.module() {
         Some(module) => module,
         None => return Ok(None),
diff --git a/examples/object_debug/Cargo.toml b/examples/object_debug/Cargo.toml
index 2880677f..9742d510 100644
--- a/examples/object_debug/Cargo.toml
+++ b/examples/object_debug/Cargo.toml
@@ -8,5 +8,4 @@ edition = "2018"
 
 [dependencies]
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic" }
diff --git a/examples/object_debug/src/main.rs b/examples/object_debug/src/main.rs
index dbe3ed22..96725ad7 100644
--- a/examples/object_debug/src/main.rs
+++ b/examples/object_debug/src/main.rs
@@ -1,20 +1,20 @@
 use std::path::Path;
 
 use clap::{App, Arg, ArgMatches};
-use failure::Error;
 
 use symbolic::common::{ByteView, DSymPathExt};
 use symbolic::debuginfo::Archive;
 
-fn print_error(error: &Error) {
+fn print_error(mut error: &dyn std::error::Error) {
     println!("Error: {}", error);
 
-    for cause in error.iter_causes() {
-        println!("   caused by {}", cause);
+    while let Some(source) = error.source() {
+        println!("   caused by {}", source);
+        error = source;
     }
 }
 
-fn inspect_object<P: AsRef<Path>>(path: P) -> Result<(), Error> {
+fn inspect_object<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn std::error::Error>> {
     let path = path.as_ref();
     println!("Inspecting {}", path.display());
 
@@ -42,7 +42,7 @@ fn inspect_object<P: AsRef<Path>>(path: P) -> Result<(), Error> {
             }
             Err(e) => {
                 print!(" - ");
-                print_error(&e.into());
+                print_error(&e);
                 continue;
             }
         }
@@ -51,17 +51,14 @@ fn inspect_object<P: AsRef<Path>>(path: P) -> Result<(), Error> {
     Ok(())
 }
 
-fn execute(matches: &ArgMatches<'_>) -> Result<(), Error> {
+fn execute(matches: &ArgMatches<'_>) {
     for path in matches.values_of("paths").unwrap_or_default() {
-        match inspect_object(path) {
-            Ok(()) => (),
-            Err(e) => print_error(&e),
+        if let Err(e) = inspect_object(path) {
+            print_error(e.as_ref())
         }
 
         println!();
     }
-
-    Ok(())
 }
 
 fn main() {
@@ -78,8 +75,5 @@ fn main() {
         )
         .get_matches();
 
-    match execute(&matches) {
-        Ok(()) => (),
-        Err(e) => print_error(&e),
-    };
+    execute(&matches);
 }
diff --git a/examples/symcache_debug/Cargo.toml b/examples/symcache_debug/Cargo.toml
index 6bb737f7..8a19bbff 100644
--- a/examples/symcache_debug/Cargo.toml
+++ b/examples/symcache_debug/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow = "1.0.32"
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic", features = ["symcache", "demangle"] }
diff --git a/examples/symcache_debug/src/main.rs b/examples/symcache_debug/src/main.rs
index b0e9e4ee..3e0d6923 100644
--- a/examples/symcache_debug/src/main.rs
+++ b/examples/symcache_debug/src/main.rs
@@ -3,15 +3,15 @@ use std::io::{Cursor, Write};
 use std::path::Path;
 use std::u64;
 
+use anyhow::{anyhow, Result};
 use clap::{App, Arg, ArgMatches};
-use failure::{err_msg, Error};
 
 use symbolic::common::{Arch, ByteView, DSymPathExt, Language};
 use symbolic::debuginfo::Archive;
 use symbolic::demangle::Demangle;
 use symbolic::symcache::{SymCache, SymCacheWriter};
 
-fn execute(matches: &ArgMatches) -> Result<(), Error> {
+fn execute(matches: &ArgMatches) -> Result<()> {
     let buffer;
     let symcache;
 
@@ -53,7 +53,7 @@ fn execute(matches: &ArgMatches) -> Result<(), Error> {
 
         let obj = match obj {
             Some(obj) => obj,
-            None => return Err(err_msg(format!("did not find architecture {}", arch))),
+            None => return Err(anyhow!("did not find architecture {}", arch)),
         };
 
         let writer = SymCacheWriter::write_object(obj, Cursor::new(Vec::new()))?;
@@ -73,7 +73,7 @@ fn execute(matches: &ArgMatches) -> Result<(), Error> {
         buffer = ByteView::open(file_path)?;
         symcache = SymCache::parse(&buffer)?;
     } else {
-        return Err(err_msg("No debug file or sym cache provided"));
+        return Err(anyhow!("No debug file or sym cache provided"));
     }
 
     // report
diff --git a/examples/unreal_engine_crash/Cargo.toml b/examples/unreal_engine_crash/Cargo.toml
index 3e2c5c44..0cd06f86 100644
--- a/examples/unreal_engine_crash/Cargo.toml
+++ b/examples/unreal_engine_crash/Cargo.toml
@@ -8,5 +8,4 @@ edition = "2018"
 
 [dependencies]
 clap = "2.33.0"
-failure = "0.1.7"
 symbolic = { path = "../../symbolic", features = ["unreal"] }
diff --git a/examples/unreal_engine_crash/src/main.rs b/examples/unreal_engine_crash/src/main.rs
index 26371e8f..473e1d77 100644
--- a/examples/unreal_engine_crash/src/main.rs
+++ b/examples/unreal_engine_crash/src/main.rs
@@ -3,11 +3,10 @@ use std::fs::File;
 use std::io::Read;
 
 use clap::{App, Arg, ArgMatches};
-use failure::Error;
 
 use symbolic::unreal::{Unreal4Crash, Unreal4FileType};
 
-fn execute(matches: &ArgMatches) -> Result<(), Error> {
+fn execute(matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
     let crash_file_path = matches.value_of("crash_file_path").unwrap();
 
     let mut file = File::open(crash_file_path)?;
diff --git a/py/symbolic/proguard.py b/py/symbolic/proguard.py
index e6c82a0c..63d247ac 100644
--- a/py/symbolic/proguard.py
+++ b/py/symbolic/proguard.py
@@ -6,60 +6,10 @@ from symbolic.utils import (
     encode_str,
     decode_uuid,
     encode_path,
-    attached_refs,
 )
 
 
-__all__ = ["ProguardMappingView", "ProguardMapper", "JavaStackFrame"]
-
-
-class ProguardMappingView(RustObject):
-    """Gives access to proguard mapping files.
-
-    DEPRECATED: Use the `ProguardMapper` class instead.
-    """
-
-    __dealloc_func__ = lib.symbolic_proguardmappingview_free
-
-    @classmethod
-    def from_bytes(cls, data):
-        """Constructs a mapping file from bytes."""
-        data = bytes(data)
-        rv = cls._from_objptr(
-            rustcall(lib.symbolic_proguardmappingview_from_bytes, data, len(data))
-        )
-        # we need to keep this reference alive or we crash. hard.
-        attached_refs[rv] = data
-        return rv
-
-    @classmethod
-    def open(cls, path):
-        """Constructs a mapping file from a path."""
-        return cls._from_objptr(
-            rustcall(lib.symbolic_proguardmappingview_open, encode_path(path))
-        )
-
-    @property
-    def uuid(self):
-        """Returns the UUID of the file."""
-        return decode_uuid(self._methodcall(lib.symbolic_proguardmappingview_get_uuid))
-
-    @property
-    def has_line_info(self):
-        """True if the file contains line information."""
-        return bool(self._methodcall(lib.symbolic_proguardmappingview_has_line_info))
-
-    def lookup(self, dotted_path, lineno=None):
-        """Given a dotted path and an optional line number this resolves
-        to the original dotted path.
-        """
-        result = self._methodcall(
-            lib.symbolic_proguardmappingview_convert_dotted_path,
-            encode_str(dotted_path),
-            lineno or 0,
-        )
-
-        return decode_str(result, free=True)
+__all__ = ["ProguardMapper", "JavaStackFrame"]
 
 
 class JavaStackFrame(object):
diff --git a/symbolic-cabi/Cargo.toml b/symbolic-cabi/Cargo.toml
index c8c17085..9b788422 100644
--- a/symbolic-cabi/Cargo.toml
+++ b/symbolic-cabi/Cargo.toml
@@ -21,7 +21,6 @@ crate-type = ["cdylib"]
 
 [dependencies]
 serde_json = "1.0.40"
-failure = "0.1.5"
 apple-crash-report-parser = { version = "0.4.0", features = ["with_serde"] }
-symbolic = { version = "7.5.0", path = "../symbolic", features = ["debuginfo", "demangle", "minidump", "proguard", "sourcemap", "symcache", "unreal-serde"] }
+symbolic = { version = "7.5.0", path = "../symbolic", features = ["debuginfo", "demangle", "minidump", "sourcemap", "symcache", "unreal-serde"] }
 proguard = { version = "4.0.1", features = ["uuid"] }
diff --git a/symbolic-cabi/include/symbolic.h b/symbolic-cabi/include/symbolic.h
index 59bc2711..9987f88b 100644
--- a/symbolic-cabi/include/symbolic.h
+++ b/symbolic-cabi/include/symbolic.h
@@ -3,7 +3,7 @@
 #ifndef SYMBOLIC_H_INCLUDED
 #define SYMBOLIC_H_INCLUDED
 
-/* Generated with cbindgen:0.14.3 */
+/* Generated with cbindgen:0.14.2 */
 
 /* Warning, this file is autogenerated. Do not modify this manually. */
 
@@ -122,11 +122,6 @@ typedef struct SymbolicObject SymbolicObject;
  */
 typedef struct SymbolicProguardMapper SymbolicProguardMapper;
 
-/**
- * Represents a proguard mapping view.
- */
-typedef struct SymbolicProguardMappingView SymbolicProguardMappingView;
-
 /**
  * Represents a sourcemap view.
  */
@@ -606,53 +601,6 @@ SymbolicProguardRemapResult symbolic_proguardmapper_remap_frame(const SymbolicPr
  */
 void symbolic_proguardmapper_result_free(SymbolicProguardRemapResult *result);
 
-/**
- * Converts a dotted path at a line number.
- *
- * @deprecated: use `symbolic_proguardmapper_remap_class` or `symbolic_proguardmapper_remap_frame` instead
- */
-SymbolicStr symbolic_proguardmappingview_convert_dotted_path(const SymbolicProguardMappingView *view,
-                                                             const SymbolicStr *path,
-                                                             uint32_t lineno);
-
-/**
- * Frees a proguard mapping view.
- *
- * @deprecated: use `symbolic_proguardmapper_free` instead
- */
-void symbolic_proguardmappingview_free(SymbolicProguardMappingView *view);
-
-/**
- * Creates a proguard mapping view from bytes.
- *
- * This shares the underlying memory and does not copy it.
- *
- * @deprecated: use `symbolic_proguardmapper_open` instead
- */
-SymbolicProguardMappingView *symbolic_proguardmappingview_from_bytes(const char *bytes,
-                                                                     uintptr_t len);
-
-/**
- * Returns the UUID of a proguard mapping file.
- *
- * @deprecated: use `symbolic_proguardmapper_get_uuid` instead
- */
-SymbolicUuid symbolic_proguardmappingview_get_uuid(SymbolicProguardMappingView *view);
-
-/**
- * Returns true if the mapping file has line infos.
- *
- * @deprecated: use `symbolic_proguardmapper_has_line_info` instead
- */
-bool symbolic_proguardmappingview_has_line_info(const SymbolicProguardMappingView *view);
-
-/**
- * Creates a proguard mapping view from a path.
- *
- * @deprecated: use `symbolic_proguardmapper_open` instead
- */
-SymbolicProguardMappingView *symbolic_proguardmappingview_open(const char *path);
-
 /**
  * Frees a source map view.
  */
diff --git a/symbolic-cabi/src/core.rs b/symbolic-cabi/src/core.rs
index d283a4f9..7e387cba 100644
--- a/symbolic-cabi/src/core.rs
+++ b/symbolic-cabi/src/core.rs
@@ -6,7 +6,6 @@ use std::ptr;
 use std::slice;
 use std::str;
 
-use failure::Error;
 use symbolic::common::Uuid;
 
 use crate::utils::{set_panic_hook, Panic, LAST_ERROR};
@@ -224,34 +223,36 @@ pub enum SymbolicErrorCode {
 impl SymbolicErrorCode {
     /// This maps all errors that can possibly happen.
     // #[allow(clippy::cyclomatic_complexity)]
-    pub fn from_error(error: &Error) -> SymbolicErrorCode {
-        for cause in error.iter_chain() {
-            if cause.downcast_ref::<Panic>().is_some() {
+    pub fn from_error(error: &(dyn std::error::Error + 'static)) -> SymbolicErrorCode {
+        let mut source = Some(error);
+
+        while let Some(error) = source {
+            if error.downcast_ref::<Panic>().is_some() {
                 return SymbolicErrorCode::Panic;
             }
 
             use std::io::Error as IoError;
-            if cause.downcast_ref::<IoError>().is_some() {
+            if error.downcast_ref::<IoError>().is_some() {
                 return SymbolicErrorCode::IoError;
             }
 
             use symbolic::common::{ParseDebugIdError, UnknownArchError, UnknownLanguageError};
-            if cause.downcast_ref::<UnknownArchError>().is_some() {
+            if error.downcast_ref::<UnknownArchError>().is_some() {
                 return SymbolicErrorCode::UnknownArchError;
-            } else if cause.downcast_ref::<UnknownLanguageError>().is_some() {
+            } else if error.downcast_ref::<UnknownLanguageError>().is_some() {
                 return SymbolicErrorCode::UnknownLanguageError;
-            } else if cause.downcast_ref::<ParseDebugIdError>().is_some() {
+            } else if error.downcast_ref::<ParseDebugIdError>().is_some() {
                 return SymbolicErrorCode::ParseDebugIdError;
             }
 
             use symbolic::debuginfo::{
-                dwarf::DwarfErrorKind, ObjectError, UnknownFileFormatError, UnknownObjectKindError,
+                dwarf::DwarfError, ObjectError, UnknownFileFormatError, UnknownObjectKindError,
             };
-            if cause.downcast_ref::<UnknownObjectKindError>().is_some() {
+            if error.downcast_ref::<UnknownObjectKindError>().is_some() {
                 return SymbolicErrorCode::UnknownObjectKindError;
-            } else if cause.downcast_ref::<UnknownFileFormatError>().is_some() {
+            } else if error.downcast_ref::<UnknownFileFormatError>().is_some() {
                 return SymbolicErrorCode::UnknownFileFormatError;
-            } else if let Some(error) = cause.downcast_ref::<ObjectError>() {
+            } else if let Some(error) = error.downcast_ref::<ObjectError>() {
                 return match error {
                     ObjectError::UnsupportedObject => {
                         SymbolicErrorCode::ObjectErrorUnsupportedObject
@@ -261,20 +262,20 @@ impl SymbolicErrorCode {
                     ObjectError::MachO(_) => SymbolicErrorCode::ObjectErrorBadMachOObject,
                     ObjectError::Pdb(_) => SymbolicErrorCode::ObjectErrorBadPdbObject,
                     ObjectError::Pe(_) => SymbolicErrorCode::ObjectErrorBadPeObject,
-                    ObjectError::Dwarf(ref e) => match e.kind() {
-                        DwarfErrorKind::InvalidUnitRef(_) => {
+                    ObjectError::Dwarf(ref e) => match e {
+                        DwarfError::InvalidUnitRef(_) => {
                             SymbolicErrorCode::DwarfErrorInvalidUnitRef
                         }
-                        DwarfErrorKind::InvalidFileRef(_) => {
+                        DwarfError::InvalidFileRef(_) => {
                             SymbolicErrorCode::DwarfErrorInvalidFileRef
                         }
-                        DwarfErrorKind::UnexpectedInline => {
+                        DwarfError::UnexpectedInline => {
                             SymbolicErrorCode::DwarfErrorUnexpectedInline
                         }
-                        DwarfErrorKind::InvertedFunctionRange => {
+                        DwarfError::InvertedFunctionRange => {
                             SymbolicErrorCode::DwarfErrorInvertedFunctionRange
                         }
-                        DwarfErrorKind::CorruptedData => SymbolicErrorCode::DwarfErrorCorruptedData,
+                        DwarfError::CorruptedData(_) => SymbolicErrorCode::DwarfErrorCorruptedData,
                         _ => SymbolicErrorCode::DwarfErrorUnknown,
                     },
                     ObjectError::SourceBundle(_) => SymbolicErrorCode::ObjectErrorBadSourceBundle,
@@ -282,24 +283,24 @@ impl SymbolicErrorCode {
                 };
             }
 
-            use symbolic::minidump::cfi::{CfiError, CfiErrorKind};
-            if let Some(error) = cause.downcast_ref::<CfiError>() {
-                return match error.kind() {
-                    CfiErrorKind::MissingDebugInfo => SymbolicErrorCode::CfiErrorMissingDebugInfo,
-                    CfiErrorKind::UnsupportedDebugFormat => {
+            use symbolic::minidump::cfi::CfiError;
+            if let Some(error) = error.downcast_ref::<CfiError>() {
+                return match error {
+                    CfiError::MissingDebugInfo => SymbolicErrorCode::CfiErrorMissingDebugInfo,
+                    CfiError::UnsupportedDebugFormat => {
                         SymbolicErrorCode::CfiErrorUnsupportedDebugFormat
                     }
-                    CfiErrorKind::BadDebugInfo => SymbolicErrorCode::CfiErrorBadDebugInfo,
-                    CfiErrorKind::UnsupportedArch => SymbolicErrorCode::CfiErrorUnsupportedArch,
-                    CfiErrorKind::InvalidAddress => SymbolicErrorCode::CfiErrorInvalidAddress,
-                    CfiErrorKind::WriteError => SymbolicErrorCode::CfiErrorWriteError,
-                    CfiErrorKind::BadFileMagic => SymbolicErrorCode::CfiErrorBadFileMagic,
+                    CfiError::BadDebugInfo(_) => SymbolicErrorCode::CfiErrorBadDebugInfo,
+                    CfiError::UnsupportedArch(_) => SymbolicErrorCode::CfiErrorUnsupportedArch,
+                    CfiError::InvalidAddress => SymbolicErrorCode::CfiErrorInvalidAddress,
+                    CfiError::WriteError(_) => SymbolicErrorCode::CfiErrorWriteError,
+                    CfiError::BadFileMagic => SymbolicErrorCode::CfiErrorBadFileMagic,
                     _ => SymbolicErrorCode::CfiErrorUnknown,
                 };
             }
 
             use symbolic::minidump::processor::{ProcessMinidumpError, ProcessResult};
-            if let Some(error) = cause.downcast_ref::<ProcessMinidumpError>() {
+            if let Some(error) = error.downcast_ref::<ProcessMinidumpError>() {
                 return match error.kind() {
                     // `Ok` is not used in errors
                     ProcessResult::Ok => SymbolicErrorCode::Unknown,
@@ -328,37 +329,37 @@ impl SymbolicErrorCode {
             }
 
             use symbolic::sourcemap::ParseSourceMapError;
-            if cause.downcast_ref::<ParseSourceMapError>().is_some() {
+            if error.downcast_ref::<ParseSourceMapError>().is_some() {
                 return SymbolicErrorCode::ParseSourceMapError;
             }
 
-            use symbolic::symcache::{SymCacheError, SymCacheErrorKind};
-            if let Some(error) = cause.downcast_ref::<SymCacheError>() {
-                return match error.kind() {
-                    SymCacheErrorKind::BadFileMagic => SymbolicErrorCode::SymCacheErrorBadFileMagic,
-                    SymCacheErrorKind::BadFileHeader => {
+            use symbolic::symcache::SymCacheError;
+            if let Some(error) = error.downcast_ref::<SymCacheError>() {
+                return match error {
+                    SymCacheError::BadFileMagic => SymbolicErrorCode::SymCacheErrorBadFileMagic,
+                    SymCacheError::BadFileHeader(_) => {
                         SymbolicErrorCode::SymCacheErrorBadFileHeader
                     }
-                    SymCacheErrorKind::BadSegment => SymbolicErrorCode::SymCacheErrorBadSegment,
-                    SymCacheErrorKind::BadCacheFile => SymbolicErrorCode::SymCacheErrorBadCacheFile,
-                    SymCacheErrorKind::UnsupportedVersion => {
+                    SymCacheError::BadSegment => SymbolicErrorCode::SymCacheErrorBadSegment,
+                    SymCacheError::BadCacheFile => SymbolicErrorCode::SymCacheErrorBadCacheFile,
+                    SymCacheError::UnsupportedVersion => {
                         SymbolicErrorCode::SymCacheErrorUnsupportedVersion
                     }
-                    SymCacheErrorKind::BadDebugFile => SymbolicErrorCode::SymCacheErrorBadDebugFile,
-                    SymCacheErrorKind::MissingDebugSection => {
+                    SymCacheError::BadDebugFile(_) => SymbolicErrorCode::SymCacheErrorBadDebugFile,
+                    SymCacheError::MissingDebugSection => {
                         SymbolicErrorCode::SymCacheErrorMissingDebugSection
                     }
-                    SymCacheErrorKind::MissingDebugInfo => {
+                    SymCacheError::MissingDebugInfo => {
                         SymbolicErrorCode::SymCacheErrorMissingDebugInfo
                     }
-                    SymCacheErrorKind::UnsupportedDebugKind => {
+                    SymCacheError::UnsupportedDebugKind => {
                         SymbolicErrorCode::SymCacheErrorUnsupportedDebugKind
                     }
-                    SymCacheErrorKind::ValueTooLarge(_) => {
+                    SymCacheError::ValueTooLarge(_) => {
                         SymbolicErrorCode::SymCacheErrorValueTooLarge
                     }
-                    SymCacheErrorKind::WriteFailed => SymbolicErrorCode::SymCacheErrorWriteFailed,
-                    SymCacheErrorKind::TooManyValues(_) => {
+                    SymCacheError::WriteFailed(_) => SymbolicErrorCode::SymCacheErrorWriteFailed,
+                    SymCacheError::TooManyValues(_) => {
                         SymbolicErrorCode::SymCacheErrorTooManyValues
                     }
                     _ => SymbolicErrorCode::SymCacheErrorUnknown,
@@ -366,7 +367,7 @@ impl SymbolicErrorCode {
             }
 
             use symbolic::unreal::Unreal4Error;
-            if let Some(error) = cause.downcast_ref::<Unreal4Error>() {
+            if let Some(error) = error.downcast_ref::<Unreal4Error>() {
                 return match error {
                     Unreal4Error::Empty => SymbolicErrorCode::Unreal4ErrorEmpty,
                     Unreal4Error::BadCompression(_) => {
@@ -383,7 +384,7 @@ impl SymbolicErrorCode {
             }
 
             use apple_crash_report_parser::ParseError;
-            if let Some(error) = cause.downcast_ref::<ParseError>() {
+            if let Some(error) = error.downcast_ref::<ParseError>() {
                 return match error {
                     ParseError::Io(_) => SymbolicErrorCode::AppleCrashReportParseErrorIo,
                     ParseError::InvalidIncidentIdentifier(_) => {
@@ -400,6 +401,8 @@ impl SymbolicErrorCode {
                     }
                 };
             }
+
+            source = error.source();
         }
 
         SymbolicErrorCode::Unknown
@@ -419,7 +422,7 @@ pub unsafe extern "C" fn symbolic_init() {
 pub unsafe extern "C" fn symbolic_err_get_last_code() -> SymbolicErrorCode {
     LAST_ERROR.with(|e| {
         if let Some(ref err) = *e.borrow() {
-            SymbolicErrorCode::from_error(err)
+            SymbolicErrorCode::from_error(err.as_ref())
         } else {
             SymbolicErrorCode::NoError
         }
@@ -435,9 +438,11 @@ pub unsafe extern "C" fn symbolic_err_get_last_message() -> SymbolicStr {
     use std::fmt::Write;
     LAST_ERROR.with(|e| {
         if let Some(ref err) = *e.borrow() {
+            let mut err = err.as_ref();
             let mut msg = err.to_string();
-            for cause in err.iter_causes() {
+            while let Some(cause) = err.source() {
                 write!(&mut msg, "\n  caused by: {}", cause).ok();
+                err = cause;
             }
             SymbolicStr::from_string(msg)
         } else {
@@ -449,21 +454,7 @@ pub unsafe extern "C" fn symbolic_err_get_last_message() -> SymbolicStr {
 /// Returns the panic information as string.
 #[no_mangle]
 pub unsafe extern "C" fn symbolic_err_get_backtrace() -> SymbolicStr {
-    LAST_ERROR.with(|e| {
-        if let Some(ref error) = *e.borrow() {
-            let backtrace = error.backtrace().to_string();
-            if !backtrace.is_empty() {
-                use std::fmt::Write;
-                let mut out = String::new();
-                write!(&mut out, "stacktrace: {}", backtrace).ok();
-                SymbolicStr::from_string(out)
-            } else {
-                Default::default()
-            }
-        } else {
-            Default::default()
-        }
-    })
+    SymbolicStr::default()
 }
 
 /// Clears the last error.
diff --git a/symbolic-cabi/src/proguard.rs b/symbolic-cabi/src/proguard.rs
index 8ff121cd..4a167ffb 100644
--- a/symbolic-cabi/src/proguard.rs
+++ b/symbolic-cabi/src/proguard.rs
@@ -1,97 +1,11 @@
-#![allow(deprecated)]
-
 use std::ffi::CStr;
 use std::os::raw::c_char;
-use std::slice;
 
 use symbolic::common::{AsSelf, ByteView, SelfCell};
-use symbolic::proguard::ProguardMappingView;
 
 use crate::core::{SymbolicStr, SymbolicUuid};
 use crate::utils::ForeignObject;
 
-/// Represents a proguard mapping view.
-pub struct SymbolicProguardMappingView;
-
-impl ForeignObject for SymbolicProguardMappingView {
-    type RustObject = ProguardMappingView<'static>;
-}
-
-ffi_fn! {
-    /// Creates a proguard mapping view from a path.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_open` instead
-    unsafe fn symbolic_proguardmappingview_open(
-        path: *const c_char
-    ) -> Result<*mut SymbolicProguardMappingView> {
-        let byteview = ByteView::open(CStr::from_ptr(path).to_str()?)?;
-        let proguard = ProguardMappingView::parse(byteview)?;
-        Ok(SymbolicProguardMappingView::from_rust(proguard))
-    }
-}
-
-ffi_fn! {
-    /// Creates a proguard mapping view from bytes.
-    ///
-    /// This shares the underlying memory and does not copy it.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_open` instead
-    unsafe fn symbolic_proguardmappingview_from_bytes(
-        bytes: *const c_char,
-        len: usize
-    ) -> Result<*mut SymbolicProguardMappingView> {
-        let slice = slice::from_raw_parts(bytes as *const _, len);
-        let byteview = ByteView::from_slice(slice);
-        let proguard = ProguardMappingView::parse(byteview)?;
-        Ok(SymbolicProguardMappingView::from_rust(proguard))
-    }
-}
-
-ffi_fn! {
-    /// Frees a proguard mapping view.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_free` instead
-    unsafe fn symbolic_proguardmappingview_free(view: *mut SymbolicProguardMappingView) {
-        SymbolicProguardMappingView::drop(view);
-    }
-}
-
-ffi_fn! {
-    /// Returns the UUID of a proguard mapping file.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_get_uuid` instead
-    unsafe fn symbolic_proguardmappingview_get_uuid(
-        view: *mut SymbolicProguardMappingView,
-    ) -> Result<SymbolicUuid> {
-        Ok(SymbolicProguardMappingView::as_rust(view).uuid().into())
-    }
-}
-
-ffi_fn! {
-    /// Converts a dotted path at a line number.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_remap_class` or `symbolic_proguardmapper_remap_frame` instead
-    unsafe fn symbolic_proguardmappingview_convert_dotted_path(
-        view: *const SymbolicProguardMappingView,
-        path: *const SymbolicStr,
-        lineno: u32,
-    ) -> Result<SymbolicStr> {
-        let path = (*path).as_str();
-        Ok(SymbolicProguardMappingView::as_rust(view).convert_dotted_path(path, lineno).into())
-    }
-}
-
-ffi_fn! {
-    /// Returns true if the mapping file has line infos.
-    ///
-    /// @deprecated: use `symbolic_proguardmapper_has_line_info` instead
-    unsafe fn symbolic_proguardmappingview_has_line_info(
-        view: *const SymbolicProguardMappingView,
-    ) -> Result<bool> {
-        Ok(SymbolicProguardMappingView::as_rust(view).has_line_info())
-    }
-}
-
 use proguard::{ProguardMapper, ProguardMapping, StackFrame};
 
 /// Represents a Java Stack Frame.
diff --git a/symbolic-cabi/src/utils.rs b/symbolic-cabi/src/utils.rs
index 9e79fd4e..afd239d7 100644
--- a/symbolic-cabi/src/utils.rs
+++ b/symbolic-cabi/src/utils.rs
@@ -3,7 +3,7 @@ use std::mem;
 use std::panic;
 use std::thread;
 
-use failure::{Error, Fail};
+type Error = Box<dyn std::error::Error + 'static>;
 
 thread_local! {
     pub static LAST_ERROR: RefCell<Option<Error>> = RefCell::new(None);
@@ -46,10 +46,17 @@ pub trait ForeignObject: Sized {
 }
 
 /// An error thrown by `landingpad` in place of panics.
-#[derive(Fail, Debug)]
-#[fail(display = "symbolic panicked: {}", _0)]
+#[derive(Debug)]
 pub struct Panic(String);
 
+impl std::fmt::Display for Panic {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "symbolic panicked: {}", self.0)
+    }
+}
+
+impl std::error::Error for Panic {}
+
 fn set_last_error(err: Error) {
     LAST_ERROR.with(|e| {
         *e.borrow_mut() = Some(err);
diff --git a/symbolic-common/Cargo.toml b/symbolic-common/Cargo.toml
index 06fdda02..6af0756d 100644
--- a/symbolic-common/Cargo.toml
+++ b/symbolic-common/Cargo.toml
@@ -22,7 +22,6 @@ all-features = true
 
 [dependencies]
 debugid = "0.7.1"
-failure = "0.1.5"
 memmap = "0.7.0"
 stable_deref_trait = "1.1.1"
 serde_ = { package = "serde", version = "1.0.88", optional = true, features = ["derive"] }
@@ -33,7 +32,6 @@ symbolic-testutils = { path = "../symbolic-testutils" }
 tempfile = "3.1.0"
 
 [features]
-default = []
 serde = ["serde_", "debugid/serde"]
 
 [badges]
diff --git a/symbolic-common/src/byteview.rs b/symbolic-common/src/byteview.rs
index 27fe0d89..7f23dcd1 100644
--- a/symbolic-common/src/byteview.rs
+++ b/symbolic-common/src/byteview.rs
@@ -236,11 +236,10 @@ mod tests {
 
     use std::io::Write;
 
-    use failure::Error;
     use tempfile::NamedTempFile;
 
     #[test]
-    fn test_open_empty_file() -> Result<(), Error> {
+    fn test_open_empty_file() -> Result<(), std::io::Error> {
         let tmp = NamedTempFile::new()?;
 
         let view = ByteView::open(&tmp.path())?;
@@ -250,7 +249,7 @@ mod tests {
     }
 
     #[test]
-    fn test_open_file() -> Result<(), Error> {
+    fn test_open_file() -> Result<(), std::io::Error> {
         let mut tmp = NamedTempFile::new()?;
 
         tmp.write_all(b"1234")?;
diff --git a/symbolic-common/src/fail.rs b/symbolic-common/src/fail.rs
deleted file mode 100644
index 1f385e63..00000000
--- a/symbolic-common/src/fail.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-/// Defines an error type with a failure context and a kind.
-///
-/// The kind enum needs to be defined explicitly, and has to implement `Fail` already. This macro
-/// then defines an error type that wraps the kind in a `failure::Context` which makes it carry a
-/// backtrace and allows nesting errors, and also defines the following methods on the error type:
-///
-///  - `kind()`: Returns a reference to the error kind.
-///  - `cause()`: Returns the causing error, if any.
-///  - `backtrace()`: Returns the backtrace of this error (or the cause), if
-///    `RUST_BACKTRACE` was set.
-///
-/// In addition to this, the following conversions are defined for the error type:
-///  - `From<ErrorKind>`
-///  - `From<Context<ErrorKind>>`
-///
-/// # Examples
-///
-/// ```
-/// use failure::Fail;
-/// use symbolic_common::derive_failure;
-///
-/// #[derive(Debug, Fail)]
-/// enum MyErrorKind {
-///     #[fail(display = "some")] Something,
-///     #[fail(display = "else")] Else,
-/// }
-///
-/// derive_failure!(MyError, MyErrorKind);
-///
-/// fn something() -> Result<(), MyError> {
-///     Err(MyErrorKind::Something.into())
-/// }
-/// ```
-#[macro_export]
-macro_rules! derive_failure {
-    ($error:ident, $kind:ident $(, $meta:meta)* $(,)?) => {
-        $(#[$meta])*
-        #[derive(Debug)]
-        pub struct $error {
-            inner: ::failure::Context<$kind>,
-        }
-
-        impl ::failure::Fail for $error {
-            fn cause(&self) -> Option<&dyn ::failure::Fail> {
-                self.inner.cause()
-            }
-
-            fn backtrace(&self) -> Option<&::failure::Backtrace> {
-                self.inner.backtrace()
-            }
-        }
-
-        impl ::std::fmt::Display for $error {
-            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
-                self.inner.fmt(f)
-            }
-        }
-
-        impl $error {
-            /// Returns the error kind of this error.
-            pub fn kind(&self) -> &$kind {
-                &self.inner.get_context()
-            }
-        }
-
-        impl From<$kind> for $error {
-            fn from(kind: $kind) -> Self {
-                $error {
-                    inner: ::failure::Context::new(kind),
-                }
-            }
-        }
-
-        impl From<::failure::Context<$kind>> for $error {
-            fn from(inner: ::failure::Context<$kind>) -> Self {
-                $error { inner }
-            }
-        }
-    };
-}
diff --git a/symbolic-common/src/lib.rs b/symbolic-common/src/lib.rs
index 1c0dd107..9f828714 100644
--- a/symbolic-common/src/lib.rs
+++ b/symbolic-common/src/lib.rs
@@ -24,14 +24,12 @@
 
 mod byteview;
 mod cell;
-mod fail;
 mod heuristics;
 mod path;
 mod types;
 
 pub use crate::byteview::*;
 pub use crate::cell::*;
-pub use crate::fail::*;
 pub use crate::heuristics::*;
 pub use crate::path::*;
 pub use crate::types::*;
diff --git a/symbolic-common/src/types.rs b/symbolic-common/src/types.rs
index 7af465a4..3704ea64 100644
--- a/symbolic-common/src/types.rs
+++ b/symbolic-common/src/types.rs
@@ -4,8 +4,6 @@ use std::borrow::Cow;
 use std::fmt;
 use std::str;
 
-use failure::Fail;
-
 #[cfg(feature = "serde")]
 use serde_::{Deserialize, Serialize};
 
@@ -223,10 +221,17 @@ impl Default for CpuFamily {
 }
 
 /// An error returned for an invalid [`Arch`](enum.Arch.html).
-#[derive(Clone, Copy, Debug, Fail)]
-#[fail(display = "unknown architecture")]
+#[derive(Debug)]
 pub struct UnknownArchError;
 
+impl fmt::Display for UnknownArchError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "unknown architecture")
+    }
+}
+
+impl std::error::Error for UnknownArchError {}
+
 /// An enumeration of CPU architectures and variants.
 ///
 /// The architectues are grouped into families, which can be retrieved by [`cpu_family`]. There are
@@ -516,10 +521,17 @@ impl str::FromStr for Arch {
 }
 
 /// An error returned for an invalid [`Language`](enum.Language.html).
-#[derive(Clone, Copy, Debug, Fail)]
-#[fail(display = "unknown language")]
+#[derive(Debug)]
 pub struct UnknownLanguageError;
 
+impl fmt::Display for UnknownLanguageError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "unknown language")
+    }
+}
+
+impl std::error::Error for UnknownLanguageError {}
+
 /// A programming language declared in debugging information.
 ///
 /// In the context of function names or source code, the lanugage can help to determine appropriate
diff --git a/symbolic-debuginfo/Cargo.toml b/symbolic-debuginfo/Cargo.toml
index 0ea3020d..1d5d4973 100644
--- a/symbolic-debuginfo/Cargo.toml
+++ b/symbolic-debuginfo/Cargo.toml
@@ -24,7 +24,6 @@ all-features = true
 
 [dependencies]
 dmsort = "1.0.0"
-failure = "0.1.5"
 fallible-iterator = "0.2.0"
 flate2 = { version = "1.0.13", features = ["rust_backend"], default-features = false }
 gimli = { version = "0.21.0", features = ["read", "std"], default-features = false }
@@ -40,6 +39,7 @@ serde = { version = "1.0.94", features = ["derive"] }
 serde_json = "1.0.40"
 smallvec = "1.2.0"
 symbolic-common = { version = "7.5.0", path = "../symbolic-common" }
+thiserror = "1.0.20"
 zip = "0.5.2"
 
 [dev-dependencies]
diff --git a/symbolic-debuginfo/src/base.rs b/symbolic-debuginfo/src/base.rs
index bccbeba4..7c8abb58 100644
--- a/symbolic-debuginfo/src/base.rs
+++ b/symbolic-debuginfo/src/base.rs
@@ -4,15 +4,20 @@ use std::iter::FromIterator;
 use std::ops::{Bound, Deref, RangeBounds};
 use std::str::FromStr;
 
-use failure::Fail;
-
 use symbolic_common::{clean_path, join_path, Arch, CodeId, DebugId, Name};
 
 /// An error returned for unknown or invalid `ObjectKinds`.
-#[derive(Debug, Fail, Clone, Copy)]
-#[fail(display = "unknown object class")]
+#[derive(Debug)]
 pub struct UnknownObjectKindError;
 
+impl fmt::Display for UnknownObjectKindError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "unknown object class")
+    }
+}
+
+impl std::error::Error for UnknownObjectKindError {}
+
 /// Represents the designated use of the object file and hints at its contents.
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
 pub enum ObjectKind {
@@ -123,10 +128,17 @@ impl FromStr for ObjectKind {
 }
 
 /// An error returned for unknown or invalid [`FileFormats`](enum.FileFormat.html).
-#[derive(Debug, Fail, Clone, Copy)]
-#[fail(display = "unknown file format")]
+#[derive(Debug)]
 pub struct UnknownFileFormatError;
 
+impl fmt::Display for UnknownFileFormatError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "unknown file format")
+    }
+}
+
+impl std::error::Error for UnknownFileFormatError {}
+
 /// Represents the physical object file format.
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
 pub enum FileFormat {
diff --git a/symbolic-debuginfo/src/breakpad.rs b/symbolic-debuginfo/src/breakpad.rs
index 085b4100..c33e4294 100644
--- a/symbolic-debuginfo/src/breakpad.rs
+++ b/symbolic-debuginfo/src/breakpad.rs
@@ -5,10 +5,10 @@ use std::collections::BTreeMap;
 use std::fmt;
 use std::str;
 
-use failure::Fail;
 use pest::Parser;
+use thiserror::Error;
 
-use symbolic_common::{derive_failure, Arch, AsSelf, CodeId, DebugId, Name};
+use symbolic_common::{Arch, AsSelf, CodeId, DebugId, Name};
 
 use crate::base::*;
 use crate::private::{Lines, Parse};
@@ -32,45 +32,27 @@ const BREAKPAD_HEADER_CAP: usize = 320;
 /// Placeholder used for missing function or symbol names.
 const UNKNOWN_NAME: &str = "<unknown>";
 
-/// Variants of `BreakpadError`.
+/// An error when dealing with [`BreakpadObject`](struct.BreakpadObject.html).
 #[non_exhaustive]
-#[derive(Debug, Fail)]
-pub enum BreakpadErrorKind {
+#[derive(Debug, Error)]
+pub enum BreakpadError {
     /// The symbol header (`MODULE` record) is missing.
-    #[fail(display = "missing breakpad symbol header")]
+    #[error("missing breakpad symbol header")]
     InvalidMagic,
 
     /// A part of the file is not encoded in valid UTF-8.
-    #[fail(display = "bad utf-8 sequence")]
-    BadEncoding(#[fail(cause)] str::Utf8Error),
+    #[error("bad utf-8 sequence")]
+    BadEncoding(#[from] str::Utf8Error),
 
     /// A record violates the Breakpad symbol syntax.
-    #[fail(display = "{}", _0)]
-    BadSyntax(pest::error::Error<Rule>),
+    #[error(transparent)]
+    BadSyntax(#[from] pest::error::Error<Rule>),
 
     /// Parsing of a record failed.
-    #[fail(display = "{}", _0)]
+    #[error("{0}")]
     Parse(&'static str),
 }
 
-derive_failure!(
-    BreakpadError,
-    BreakpadErrorKind,
-    doc = "An error when dealing with [`BreakpadObject`](struct.BreakpadObject.html).",
-);
-
-impl From<str::Utf8Error> for BreakpadError {
-    fn from(error: str::Utf8Error) -> Self {
-        BreakpadErrorKind::BadEncoding(error).into()
-    }
-}
-
-impl From<pest::error::Error<Rule>> for BreakpadError {
-    fn from(error: pest::error::Error<Rule>) -> Self {
-        BreakpadErrorKind::BadSyntax(error).into()
-    }
-}
-
 // TODO(ja): Test the parser
 
 /// A [module record], constituting the header of a Breakpad file.
@@ -152,7 +134,7 @@ impl<'d> BreakpadInfoRecord<'d> {
             }
         }
 
-        Err(BreakpadErrorKind::Parse("unknown INFO record").into())
+        Err(BreakpadError::Parse("unknown INFO record"))
     }
 
     fn code_info_from_pair(pair: pest::iterators::Pair<'d, Rule>) -> Result<Self, BreakpadError> {
@@ -248,7 +230,7 @@ impl<'d> BreakpadFileRecord<'d> {
             match pair.as_rule() {
                 Rule::file_id => {
                     record.id = u64::from_str_radix(pair.as_str(), 10)
-                        .map_err(|_| BreakpadErrorKind::Parse("file identifier"))?;
+                        .map_err(|_| BreakpadError::Parse("file identifier"))?;
                 }
                 Rule::name => record.name = pair.as_str(),
                 _ => unreachable!(),
@@ -324,11 +306,11 @@ impl<'d> BreakpadPublicRecord<'d> {
                 Rule::multiple => record.multiple = true,
                 Rule::addr => {
                     record.address = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("symbol address"))?;
+                        .map_err(|_| BreakpadError::Parse("symbol address"))?;
                 }
                 Rule::param_size => {
                     record.parameter_size = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("symbol parameter size"))?;
+                        .map_err(|_| BreakpadError::Parse("symbol parameter size"))?;
                 }
                 Rule::name => record.name = pair.as_str(),
                 _ => unreachable!(),
@@ -413,15 +395,15 @@ impl<'d> BreakpadFuncRecord<'d> {
                 Rule::multiple => record.multiple = true,
                 Rule::addr => {
                     record.address = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("function address"))?;
+                        .map_err(|_| BreakpadError::Parse("function address"))?;
                 }
                 Rule::size => {
                     record.size = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("function size"))?;
+                        .map_err(|_| BreakpadError::Parse("function size"))?;
                 }
                 Rule::param_size => {
                     record.parameter_size = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("function parameter size"))?;
+                        .map_err(|_| BreakpadError::Parse("function parameter size"))?;
                 }
                 Rule::name => record.name = pair.as_str(),
                 _ => unreachable!(),
@@ -536,11 +518,11 @@ impl BreakpadLineRecord {
             match pair.as_rule() {
                 Rule::addr => {
                     record.address = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("line address"))?;
+                        .map_err(|_| BreakpadError::Parse("line address"))?;
                 }
                 Rule::size => {
                     record.size = u64::from_str_radix(pair.as_str(), 16)
-                        .map_err(|_| BreakpadErrorKind::Parse("line size"))?;
+                        .map_err(|_| BreakpadError::Parse("line size"))?;
                 }
                 Rule::line_num => {
                     // NB: Breakpad does not allow negative line numbers and even tests that the
@@ -548,11 +530,11 @@ impl BreakpadLineRecord {
                     // been observed at least for ELF files, so handle them gracefully.
                     record.line = i32::from_str_radix(pair.as_str(), 10)
                         .map(|line| u64::from(line as u32))
-                        .map_err(|_| BreakpadErrorKind::Parse("line number"))?;
+                        .map_err(|_| BreakpadError::Parse("line number"))?;
                 }
                 Rule::file_id => {
                     record.file_id = u64::from_str_radix(pair.as_str(), 10)
-                        .map_err(|_| BreakpadErrorKind::Parse("file number"))?;
+                        .map_err(|_| BreakpadError::Parse("file number"))?;
                 }
                 _ => unreachable!(),
             }
@@ -786,11 +768,11 @@ impl<'d> BreakpadObject<'d> {
             id: module
                 .id
                 .parse()
-                .map_err(|_| BreakpadErrorKind::Parse("module id"))?,
+                .map_err(|_| BreakpadError::Parse("module id"))?,
             arch: module
                 .arch
                 .parse()
-                .map_err(|_| BreakpadErrorKind::Parse("module architecture"))?,
+                .map_err(|_| BreakpadError::Parse("module architecture"))?,
             module,
             data,
         })
diff --git a/symbolic-debuginfo/src/dwarf.rs b/symbolic-debuginfo/src/dwarf.rs
index 6cda0d5b..7c56f874 100644
--- a/symbolic-debuginfo/src/dwarf.rs
+++ b/symbolic-debuginfo/src/dwarf.rs
@@ -12,19 +12,20 @@ use std::fmt;
 use std::marker::PhantomData;
 use std::ops::Deref;
 
-use failure::Fail;
 use fallible_iterator::FallibleIterator;
 use gimli::read::{AttributeValue, Range};
 use gimli::{constants, UnitSectionOffset};
 use lazycell::LazyCell;
+use thiserror::Error;
 
-use symbolic_common::{derive_failure, AsSelf, Language, Name, SelfCell};
+use symbolic_common::{AsSelf, Language, Name, SelfCell};
 
 use crate::base::*;
 use crate::private::FunctionStack;
 
 #[doc(hidden)]
 pub use gimli;
+pub use gimli::read::Error as GimliError;
 pub use gimli::RunTimeEndian as Endian;
 
 type Slice<'a> = gimli::read::EndianSlice<'a, Endian>;
@@ -42,41 +43,29 @@ type IncompleteLineNumberProgram<'a> = gimli::read::IncompleteLineProgram<Slice<
 type LineNumberProgramHeader<'a> = gimli::read::LineProgramHeader<Slice<'a>>;
 type LineProgramFileEntry<'a> = gimli::read::FileEntry<Slice<'a>>;
 
-/// Variants of [`DwarfError`](struct.DwarfError.html).
+/// An error handling [`DWARF`](trait.Dwarf.html) debugging information.
 #[non_exhaustive]
-#[derive(Clone, Copy, Debug, Eq, Fail, PartialEq)]
-pub enum DwarfErrorKind {
+#[derive(Debug, Error)]
+pub enum DwarfError {
     /// A compilation unit referenced by index does not exist.
-    #[fail(display = "compilation unit for offset {} does not exist", _0)]
+    #[error("compilation unit for offset {0} does not exist")]
     InvalidUnitRef(usize),
 
     /// A file record referenced by index does not exist.
-    #[fail(display = "referenced file {} does not exist", _0)]
+    #[error("referenced file {0} does not exist")]
     InvalidFileRef(u64),
 
     /// An inline record was encountered without an inlining parent.
-    #[fail(display = "unexpected inline function without parent")]
+    #[error("unexpected inline function without parent")]
     UnexpectedInline,
 
     /// The debug_ranges of a function are invalid.
-    #[fail(display = "function with inverted address range")]
+    #[error("function with inverted address range")]
     InvertedFunctionRange,
 
     /// The DWARF file is corrupted. See the cause for more information.
-    #[fail(display = "corrupted dwarf debug data")]
-    CorruptedData,
-}
-
-derive_failure!(
-    DwarfError,
-    DwarfErrorKind,
-    doc = "An error handling [`DWARF`](trait.Dwarf.html) debugging information.",
-);
-
-impl From<gimli::read::Error> for DwarfError {
-    fn from(error: gimli::read::Error) -> Self {
-        error.context(DwarfErrorKind::CorruptedData).into()
-    }
+    #[error("corrupted dwarf debug data")]
+    CorruptedData(#[from] GimliError),
 }
 
 /// DWARF section information including its data.
@@ -526,7 +515,7 @@ impl<'d, 'a> DwarfUnit<'d, 'a> {
 
         if low_pc > high_pc {
             // TODO: consider swallowing errors here?
-            return Err(DwarfErrorKind::InvertedFunctionRange.into());
+            return Err(DwarfError::InvertedFunctionRange);
         }
 
         range_buf.push(Range {
@@ -725,7 +714,7 @@ impl<'d, 'a> DwarfUnit<'d, 'a> {
                 // indicates invalid debug information.
                 let parent = match stack.peek_mut() {
                     Some(parent) => parent,
-                    None => return Err(DwarfErrorKind::UnexpectedInline.into()),
+                    None => return Err(DwarfError::UnexpectedInline),
                 };
 
                 // Make sure there is correct line information for the call site of this inlined
@@ -1064,7 +1053,7 @@ impl<'d> DwarfInfo<'d> {
 
         let index = match search_result {
             Ok(index) => index,
-            Err(0) => return Err(DwarfErrorKind::InvalidUnitRef(offset.0).into()),
+            Err(0) => return Err(DwarfError::InvalidUnitRef(offset.0)),
             Err(next_index) => next_index - 1,
         };
 
@@ -1075,7 +1064,7 @@ impl<'d> DwarfInfo<'d> {
             }
         }
 
-        Err(DwarfErrorKind::InvalidUnitRef(offset.0).into())
+        Err(DwarfError::InvalidUnitRef(offset.0))
     }
 
     /// Returns an iterator over all compilation units.
diff --git a/symbolic-debuginfo/src/elf.rs b/symbolic-debuginfo/src/elf.rs
index 9f210f80..7688b127 100644
--- a/symbolic-debuginfo/src/elf.rs
+++ b/symbolic-debuginfo/src/elf.rs
@@ -4,10 +4,10 @@ use std::borrow::Cow;
 use std::fmt;
 use std::io::Cursor;
 
-use failure::Fail;
 use flate2::{Decompress, FlushDecompress};
 use goblin::elf::compression_header::{CompressionHeader, ELFCOMPRESS_ZLIB};
-use goblin::{container::Ctx, elf, error::Error as GoblinError, strtab};
+use goblin::{container::Ctx, elf, strtab};
+use thiserror::Error;
 
 use symbolic_common::{Arch, AsSelf, CodeId, DebugId, Uuid};
 
@@ -15,6 +15,9 @@ use crate::base::*;
 use crate::dwarf::{Dwarf, DwarfDebugSession, DwarfError, DwarfSection, Endian};
 use crate::private::Parse;
 
+#[doc(inline)]
+pub use goblin::error::Error as GoblinError;
+
 const UUID_SIZE: usize = 16;
 const PAGE_SIZE: usize = 4096;
 
@@ -37,11 +40,11 @@ const MIPS_64_FLAGS: u32 = EF_MIPS_ABI_O64 | EF_MIPS_ABI_EABI64;
 
 /// An error when dealing with [`ElfObject`](struct.ElfObject.html).
 #[non_exhaustive]
-#[derive(Debug, Fail)]
+#[derive(Debug, Error)]
 pub enum ElfError {
     /// The data in the ELF file could not be parsed.
-    #[fail(display = "invalid ELF file")]
-    BadObject(#[fail(cause)] GoblinError),
+    #[error("invalid ELF file")]
+    BadObject(#[from] GoblinError),
 }
 
 /// Executable and Linkable Format, used for executables and libraries on Linux.
@@ -61,9 +64,7 @@ impl<'d> ElfObject<'d> {
 
     /// Tries to parse an ELF object from the given slice.
     pub fn parse(data: &'d [u8]) -> Result<Self, ElfError> {
-        elf::Elf::parse(data)
-            .map(|elf| ElfObject { elf, data })
-            .map_err(ElfError::BadObject)
+        Ok(elf::Elf::parse(data).map(|elf| ElfObject { elf, data })?)
     }
 
     /// The container file format, which is always `FileFormat::Elf`.
diff --git a/symbolic-debuginfo/src/macho.rs b/symbolic-debuginfo/src/macho.rs
index 7f980438..c7d1dc0b 100644
--- a/symbolic-debuginfo/src/macho.rs
+++ b/symbolic-debuginfo/src/macho.rs
@@ -4,9 +4,9 @@ use std::borrow::Cow;
 use std::fmt;
 use std::io::Cursor;
 
-use failure::Fail;
 use goblin::{error::Error as GoblinError, mach};
 use smallvec::SmallVec;
+use thiserror::Error;
 
 use symbolic_common::{Arch, AsSelf, CodeId, DebugId, Uuid};
 
@@ -16,11 +16,11 @@ use crate::private::{MonoArchive, MonoArchiveObjects, Parse};
 
 /// An error when dealing with [`MachObject`](struct.MachObject.html).
 #[non_exhaustive]
-#[derive(Debug, Fail)]
+#[derive(Debug, Error)]
 pub enum MachError {
     /// The data in the MachO file could not be parsed.
-    #[fail(display = "invalid MachO file")]
-    BadObject(#[fail(cause)] GoblinError),
+    #[error("invalid MachO file")]
+    BadObject(#[from] GoblinError),
 }
 
 /// Mach Object containers, used for executables and debug companions on macOS and iOS.
@@ -40,9 +40,7 @@ impl<'d> MachObject<'d> {
 
     /// Tries to parse a MachO from the given slice.
     pub fn parse(data: &'d [u8]) -> Result<Self, MachError> {
-        mach::MachO::parse(data, 0)
-            .map(|macho| MachObject { macho, data })
-            .map_err(MachError::BadObject)
+        Ok(mach::MachO::parse(data, 0).map(|macho| MachObject { macho, data })?)
     }
 
     /// The container file format, which is always `FileFormat::MachO`.
@@ -488,9 +486,7 @@ impl<'d> FatMachO<'d> {
 
     /// Tries to parse a fat MachO container from the given slice.
     pub fn parse(data: &'d [u8]) -> Result<Self, MachError> {
-        mach::MultiArch::new(data)
-            .map(|fat| FatMachO { fat, data })
-            .map_err(MachError::BadObject)
+        Ok(mach::MultiArch::new(data).map(|fat| FatMachO { fat, data })?)
     }
 
     /// Returns an iterator over objects in this container.
@@ -513,7 +509,7 @@ impl<'d> FatMachO<'d> {
     /// be parsed.
     pub fn object_by_index(&self, index: usize) -> Result<Option<MachObject<'d>>, MachError> {
         let arch = match self.fat.iter_arches().nth(index) {
-            Some(arch) => arch.map_err(MachError::BadObject)?,
+            Some(arch) => arch?,
             None => return Ok(None),
         };
 
diff --git a/symbolic-debuginfo/src/object.rs b/symbolic-debuginfo/src/object.rs
index 7de19d1d..81229aa2 100644
--- a/symbolic-debuginfo/src/object.rs
+++ b/symbolic-debuginfo/src/object.rs
@@ -2,8 +2,8 @@
 
 use std::borrow::Cow;
 
-use failure::Fail;
 use goblin::Hint;
+use thiserror::Error;
 
 use symbolic_common::{Arch, AsSelf, CodeId, DebugId};
 
@@ -60,40 +60,39 @@ macro_rules! map_result {
 
 /// An error when dealing with any kind of [`Object`](enum.Object.html).
 #[non_exhaustive]
-#[allow(clippy::large_enum_variant)]
-#[derive(Debug, Fail)]
+#[derive(Debug, Error)]
 pub enum ObjectError {
     /// The object file format is not supported.
-    #[fail(display = "unsupported object file format")]
+    #[error("unsupported object file format")]
     UnsupportedObject,
 
     /// An error in a Breakpad ASCII symbol.
-    #[fail(display = "failed to process breakpad file")]
-    Breakpad(#[fail(cause)] BreakpadError),
+    #[error("failed to process breakpad file")]
+    Breakpad(#[from] BreakpadError),
 
     /// An error in an ELF file.
-    #[fail(display = "failed to process elf file")]
-    Elf(#[fail(cause)] ElfError),
+    #[error("failed to process elf file")]
+    Elf(#[from] ElfError),
 
     /// An error in a Mach object.
-    #[fail(display = "failed to process macho file")]
-    MachO(#[fail(cause)] MachError),
+    #[error("failed to process macho file")]
+    MachO(#[from] MachError),
 
     /// An error in a Program Database.
-    #[fail(display = "failed to process pdb file")]
-    Pdb(#[fail(cause)] PdbError),
+    #[error("failed to process pdb file")]
+    Pdb(#[from] PdbError),
 
     /// An error in a Portable Executable.
-    #[fail(display = "failed to process pe file")]
-    Pe(#[fail(cause)] PeError),
+    #[error("failed to process pe file")]
+    Pe(#[from] PeError),
 
     /// An error in DWARF debugging information.
-    #[fail(display = "failed to process dwarf info")]
-    Dwarf(#[fail(cause)] DwarfError),
+    #[error("failed to process dwarf info")]
+    Dwarf(#[from] DwarfError),
 
     /// An error in source bundles.
-    #[fail(display = "failed to process source bundle")]
-    SourceBundle(#[fail(cause)] SourceBundleError),
+    #[error("failed to process source bundle")]
+    SourceBundle(#[from] SourceBundleError),
 }
 
 /// Tries to infer the object type from the start of the given buffer.
diff --git a/symbolic-debuginfo/src/pdb.rs b/symbolic-debuginfo/src/pdb.rs
index 282f7b8d..c3228171 100644
--- a/symbolic-debuginfo/src/pdb.rs
+++ b/symbolic-debuginfo/src/pdb.rs
@@ -8,7 +8,6 @@ use std::fmt;
 use std::io::Cursor;
 use std::sync::Arc;
 
-use failure::Fail;
 use lazycell::LazyCell;
 use parking_lot::RwLock;
 use pdb::{
@@ -16,10 +15,9 @@ use pdb::{
     ModuleInfo, PdbInternalSectionOffset, ProcedureSymbol, SymbolData,
 };
 use smallvec::SmallVec;
+use thiserror::Error;
 
-use symbolic_common::{
-    derive_failure, Arch, AsSelf, CodeId, CpuFamily, DebugId, Name, SelfCell, Uuid,
-};
+use symbolic_common::{Arch, AsSelf, CodeId, CpuFamily, DebugId, Name, SelfCell, Uuid};
 
 use crate::base::*;
 use crate::private::{FunctionStack, Parse};
@@ -32,39 +30,21 @@ const MAGIC_BIG: &[u8] = b"Microsoft C/C++ MSF 7.00\r\n\x1a\x44\x53\x00\x00\x00"
 #[doc(hidden)]
 pub use pdb;
 
-/// Variants of [`PdbError`](struct.PdbError.html).
+/// An error when dealing with [`PdbObject`](struct.PdbObject.html).
 #[non_exhaustive]
-#[derive(Clone, Copy, Debug, Eq, Fail, PartialEq)]
-pub enum PdbErrorKind {
+#[derive(Debug, Error)]
+pub enum PdbError {
     /// The PDB file is corrupted. See the cause for more information.
-    #[fail(display = "invalid pdb file")]
-    BadObject,
+    #[error("invalid pdb file")]
+    BadObject(#[from] pdb::Error),
 
     /// An inline record was encountered without an inlining parent.
-    #[fail(display = "unexpected inline function without parent")]
+    #[error("unexpected inline function without parent")]
     UnexpectedInline,
 
     /// Formatting of a type name failed
-    #[fail(display = "failed to format type name")]
-    FormattingFailed,
-}
-
-derive_failure!(
-    PdbError,
-    PdbErrorKind,
-    doc = "An error when dealing with [`PdbObject`](struct.PdbObject.html)."
-);
-
-impl From<pdb::Error> for PdbError {
-    fn from(error: pdb::Error) -> Self {
-        error.context(PdbErrorKind::BadObject).into()
-    }
-}
-
-impl From<fmt::Error> for PdbError {
-    fn from(error: fmt::Error) -> Self {
-        error.context(PdbErrorKind::FormattingFailed).into()
-    }
+    #[error("failed to format type name")]
+    FormattingFailed(#[from] fmt::Error),
 }
 
 /// Program Database, the debug companion format on Windows.
@@ -1066,7 +1046,7 @@ impl<'s> Unit<'s> {
                     let parent_offset = proc_offsets
                         .last()
                         .map(|&(_, offset)| offset)
-                        .ok_or_else(|| PdbError::from(PdbErrorKind::UnexpectedInline))?;
+                        .ok_or(PdbError::UnexpectedInline)?;
 
                     // We can assume that inlinees will be listed in the inlinee table. If missing,
                     // skip silently instead of erroring out. Missing a single inline function is
diff --git a/symbolic-debuginfo/src/pe.rs b/symbolic-debuginfo/src/pe.rs
index dcaab2e5..0d33ae46 100644
--- a/symbolic-debuginfo/src/pe.rs
+++ b/symbolic-debuginfo/src/pe.rs
@@ -5,8 +5,8 @@ use std::fmt;
 use std::io::Cursor;
 use std::marker::PhantomData;
 
-use failure::Fail;
 use goblin::{error::Error as GoblinError, pe};
+use thiserror::Error;
 
 use symbolic_common::{Arch, AsSelf, CodeId, DebugId, Uuid};
 
@@ -18,11 +18,11 @@ pub use goblin::pe::section_table::SectionTable;
 
 /// An error when dealing with [`PeObject`](struct.PeObject.html).
 #[non_exhaustive]
-#[derive(Debug, Fail)]
+#[derive(Debug, Error)]
 pub enum PeError {
     /// The data in the PE file could not be parsed.
-    #[fail(display = "invalid PE file")]
-    BadObject(#[fail(cause)] GoblinError),
+    #[error("invalid PE file")]
+    BadObject(#[from] GoblinError),
 }
 
 /// Detects if the PE is a packer stub.
diff --git a/symbolic-debuginfo/src/sourcebundle.rs b/symbolic-debuginfo/src/sourcebundle.rs
index 7ebb00f0..d561ffc1 100644
--- a/symbolic-debuginfo/src/sourcebundle.rs
+++ b/symbolic-debuginfo/src/sourcebundle.rs
@@ -42,14 +42,14 @@ use std::io::{BufReader, BufWriter, Read, Seek, Write};
 use std::path::Path;
 use std::sync::Arc;
 
-use failure::{Fail, ResultExt};
 use lazycell::LazyCell;
 use parking_lot::Mutex;
 use regex::Regex;
 use serde::{Deserialize, Serialize};
-use zip::{write::FileOptions, ZipWriter};
+use thiserror::Error;
+use zip::{result::ZipError, write::FileOptions, ZipWriter};
 
-use symbolic_common::{derive_failure, Arch, AsSelf, CodeId, DebugId};
+use symbolic_common::{Arch, AsSelf, CodeId, DebugId};
 
 use crate::base::*;
 use crate::private::Parse;
@@ -71,28 +71,35 @@ lazy_static::lazy_static! {
     static ref SANE_PATH_RE: Regex = Regex::new(r#":?[/\\]+"#).unwrap();
 }
 
-/// Variants of [`SourceBundleError`](struct.SourceBundleError.html).
+/// An error returned when handling [`SourceBundle`](struct.SourceBundle.html).
 #[non_exhaustive]
-#[derive(Clone, Copy, Debug, Eq, Fail, PartialEq)]
-pub enum SourceBundleErrorKind {
+#[derive(Debug, Error)]
+pub enum SourceBundleError {
     /// The source bundle container is damanged.
-    #[fail(display = "malformed zip archive")]
-    BadZip,
+    #[error("malformed zip archive")]
+    BadZip(#[source] ZipError),
+
+    /// An error when reading/writing the manifest.
+    #[error("failed to read/write source bundle manifest")]
+    BadManifest(#[source] serde_json::Error),
 
     /// The `Object` contains invalid data and cannot be converted.
-    #[fail(display = "malformed debug info file")]
-    BadDebugFile,
+    #[error("malformed debug info file")]
+    BadDebugFile(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
 
     /// Generic error when writing a source bundle, most likely IO.
-    #[fail(display = "failed to write source bundle")]
-    WriteFailed,
+    #[error("failed to write source bundle")]
+    WriteFailed(#[source] ZipError),
 }
 
-derive_failure!(
-    SourceBundleError,
-    SourceBundleErrorKind,
-    doc = "An error returned when handling `SourceBundles`.",
-);
+impl SourceBundleError {
+    fn bad_debug_file<E>(err: E) -> Self
+    where
+        E: std::error::Error + Send + Sync + 'static,
+    {
+        SourceBundleError::BadDebugFile(Box::new(err))
+    }
+}
 
 /// Trims matching suffices of a string in-place.
 fn trim_end_matches<F>(string: &mut String, pat: F)
@@ -310,12 +317,12 @@ impl<'d> SourceBundle<'d> {
     /// Tries to parse a `SourceBundle` from the given slice.
     pub fn parse(data: &'d [u8]) -> Result<SourceBundle<'d>, SourceBundleError> {
         let mut archive = zip::read::ZipArchive::new(std::io::Cursor::new(data))
-            .context(SourceBundleErrorKind::BadZip)?;
+            .map_err(SourceBundleError::BadZip)?;
         let manifest_file = archive
             .by_name("manifest.json")
-            .context(SourceBundleErrorKind::BadZip)?;
+            .map_err(SourceBundleError::BadZip)?;
         let manifest =
-            serde_json::from_reader(manifest_file).context(SourceBundleErrorKind::BadZip)?;
+            serde_json::from_reader(manifest_file).map_err(SourceBundleError::BadManifest)?;
         Ok(SourceBundle {
             manifest: Arc::new(manifest),
             archive: Arc::new(Mutex::new(archive)),
@@ -608,13 +615,12 @@ impl<'d> SourceBundleDebugSession<'d> {
         let mut archive = self.archive.lock();
         let mut file = archive
             .by_name(zip_path)
-            .context(SourceBundleErrorKind::BadZip)?;
+            .map_err(SourceBundleError::BadZip)?;
         let mut source_content = String::new();
 
-        match file.read_to_string(&mut source_content) {
-            Ok(_) => Ok(Some(source_content)),
-            Err(e) => Err(e).context(SourceBundleErrorKind::BadZip)?,
-        }
+        file.read_to_string(&mut source_content)
+            .map_err(|e| SourceBundleError::BadZip(ZipError::Io(e)))?;
+        Ok(Some(source_content))
     }
 
     /// Looks up a file's source contents by its full canonicalized path.
@@ -707,9 +713,9 @@ fn sanitize_bundle_path(path: &str) -> String {
 /// Note that dropping the writer
 ///
 /// ```no_run
-/// # use failure::Error; use std::fs::File;
+/// # use std::fs::File;
 /// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
-/// # fn main() -> Result<(), Error> {
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
 ///
 /// // Add file called "foo.txt"
@@ -744,7 +750,7 @@ where
         let header = SourceBundleHeader::default();
         writer
             .write_all(header.as_bytes())
-            .context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(|e| SourceBundleError::WriteFailed(ZipError::Io(e)))?;
 
         Ok(SourceBundleWriter {
             manifest: SourceBundleManifest::new(),
@@ -810,9 +816,9 @@ where
     /// appended to the file name. Any subsequent duplicate increases that counter. For example:
     ///
     /// ```no_run
-    /// # use failure::Error; use std::fs::File;
+    /// # use std::fs::File;
     /// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
-    /// # fn main() -> Result<(), Error> {
+    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
     /// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
     ///
     /// // Add file at "foo.txt"
@@ -842,8 +848,9 @@ where
 
         self.writer
             .start_file(unique_path.clone(), FileOptions::default())
-            .context(SourceBundleErrorKind::WriteFailed)?;
-        std::io::copy(&mut file, &mut self.writer).context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(SourceBundleError::WriteFailed)?;
+        std::io::copy(&mut file, &mut self.writer)
+            .map_err(|e| SourceBundleError::WriteFailed(ZipError::Io(e)))?;
 
         self.manifest.files.insert(unique_path, info);
         Ok(())
@@ -858,7 +865,7 @@ where
     pub fn write_object<O>(self, object: &O, object_name: &str) -> Result<bool, SourceBundleError>
     where
         O: ObjectLike,
-        O::Error: Fail,
+        O::Error: std::error::Error + Send + Sync + 'static,
     {
         self.write_object_with_filter(object, object_name, |_| true)
     }
@@ -879,13 +886,13 @@ where
     ) -> Result<bool, SourceBundleError>
     where
         O: ObjectLike,
-        O::Error: Fail,
+        O::Error: std::error::Error + Send + Sync + 'static,
         F: FnMut(&FileEntry) -> bool,
     {
         let mut files_handled = BTreeSet::new();
         let session = object
             .debug_session()
-            .context(SourceBundleErrorKind::BadDebugFile)?;
+            .map_err(SourceBundleError::bad_debug_file)?;
 
         self.set_attribute("arch", object.arch().to_string());
         self.set_attribute("debug_id", object.debug_id().to_string());
@@ -895,7 +902,7 @@ where
         }
 
         for file_result in session.files() {
-            let file = file_result.context(SourceBundleErrorKind::BadDebugFile)?;
+            let file = file_result.map_err(SourceBundleError::bad_debug_file)?;
             let filename = file.abs_path_str();
 
             if files_handled.contains(&filename) {
@@ -915,8 +922,7 @@ where
                 info.set_ty(SourceFileType::Source);
                 info.set_path(filename.clone());
 
-                self.add_file(bundle_path, source, info)
-                    .context(SourceBundleErrorKind::WriteFailed)?;
+                self.add_file(bundle_path, source, info)?;
             }
 
             files_handled.insert(filename);
@@ -933,7 +939,7 @@ where
         self.write_manifest()?;
         self.writer
             .finish()
-            .context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(SourceBundleError::WriteFailed)?;
         self.finished = true;
         Ok(())
     }
@@ -969,10 +975,10 @@ where
     fn write_manifest(&mut self) -> Result<(), SourceBundleError> {
         self.writer
             .start_file(MANIFEST_PATH, FileOptions::default())
-            .context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(SourceBundleError::WriteFailed)?;
 
         serde_json::to_writer(&mut self.writer, &self.manifest)
-            .context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(SourceBundleError::BadManifest)?;
 
         Ok(())
     }
@@ -993,7 +999,7 @@ impl SourceBundleWriter<BufWriter<File>> {
             .create(true)
             .truncate(true)
             .open(path)
-            .context(SourceBundleErrorKind::WriteFailed)?;
+            .map_err(|e| SourceBundleError::WriteFailed(ZipError::Io(e)))?;
 
         Self::start(BufWriter::new(file))
     }
@@ -1005,10 +1011,8 @@ mod tests {
 
     use std::io::Cursor;
 
-    use failure::Error;
-
     #[test]
-    fn test_has_file() -> Result<(), Error> {
+    fn test_has_file() -> Result<(), SourceBundleError> {
         let writer = Cursor::new(Vec::new());
         let mut bundle = SourceBundleWriter::start(writer)?;
 
@@ -1020,7 +1024,7 @@ mod tests {
     }
 
     #[test]
-    fn test_duplicate_files() -> Result<(), Error> {
+    fn test_duplicate_files() -> Result<(), SourceBundleError> {
         let writer = Cursor::new(Vec::new());
         let mut bundle = SourceBundleWriter::start(writer)?;
 
diff --git a/symbolic-minidump/Cargo.toml b/symbolic-minidump/Cargo.toml
index 9be44855..96893555 100644
--- a/symbolic-minidump/Cargo.toml
+++ b/symbolic-minidump/Cargo.toml
@@ -30,12 +30,12 @@ include = [
 all-features = true
 
 [dependencies]
-failure = "0.1.5"
 lazy_static = "1.4.0"
 regex = "1.3.5"
 serde = { version = "1.0.94", optional = true }
 symbolic-common = { version = "7.5.0", path = "../symbolic-common" }
 symbolic-debuginfo = { version = "7.5.0", path = "../symbolic-debuginfo" }
+thiserror = "1.0.20"
 
 [build-dependencies]
 cc = { version = "1.0.50", features = ["parallel"] }
diff --git a/symbolic-minidump/src/cfi.rs b/symbolic-minidump/src/cfi.rs
index 8b144dfe..c067ea6c 100644
--- a/symbolic-minidump/src/cfi.rs
+++ b/symbolic-minidump/src/cfi.rs
@@ -21,19 +21,19 @@ use std::collections::HashMap;
 use std::io::{self, Write};
 use std::ops::Range;
 
-use failure::{Fail, ResultExt};
-
-use symbolic_common::{derive_failure, Arch, ByteView, UnknownArchError};
-use symbolic_debuginfo::breakpad::{BreakpadObject, BreakpadStackRecord};
+use symbolic_common::{Arch, ByteView, UnknownArchError};
+use symbolic_debuginfo::breakpad::{BreakpadError, BreakpadObject, BreakpadStackRecord};
 use symbolic_debuginfo::dwarf::gimli::{
     BaseAddresses, CfaRule, CieOrFde, DebugFrame, EhFrame, Error, FrameDescriptionEntry, Reader,
     Register, RegisterRule, UninitializedUnwindContext, UnwindSection,
 };
-use symbolic_debuginfo::dwarf::Dwarf;
+use symbolic_debuginfo::dwarf::{Dwarf, DwarfError, GimliError};
+use symbolic_debuginfo::elf::{ElfError, GoblinError};
 use symbolic_debuginfo::pdb::pdb::{self, FallibleIterator, FrameData, Rva, StringTable};
-use symbolic_debuginfo::pdb::PdbObject;
+use symbolic_debuginfo::pdb::{PdbError, PdbObject};
 use symbolic_debuginfo::pe::{PeObject, RuntimeFunction, UnwindOperation};
-use symbolic_debuginfo::{Object, ObjectLike};
+use symbolic_debuginfo::{Object, ObjectError, ObjectLike};
+use thiserror::Error;
 
 /// The latest version of the file format.
 pub const CFICACHE_LATEST_VERSION: u32 = 1;
@@ -45,48 +45,60 @@ const EMPTY_FUNCTION: RuntimeFunction = RuntimeFunction {
     unwind_info_address: 0,
 };
 
-/// Possible error kinds of `CfiError`.
+/// An error returned by [`AsciiCfiWriter`](struct.AsciiCfiWriter.html).
 #[non_exhaustive]
-#[derive(Debug, Fail, Copy, Clone)]
-pub enum CfiErrorKind {
+#[derive(Debug, Error)]
+pub enum CfiError {
     /// Required debug sections are missing in the `Object` file.
-    #[fail(display = "missing cfi debug sections")]
+    #[error("missing cfi debug sections")]
     MissingDebugInfo,
 
     /// The debug information in the `Object` file is not supported.
-    #[fail(display = "unsupported debug format")]
+    #[error("unsupported debug format")]
     UnsupportedDebugFormat,
 
     /// The debug information in the `Object` file is invalid.
-    #[fail(display = "bad debug information")]
-    BadDebugInfo,
+    #[error("bad debug information")]
+    BadDebugInfo(#[from] Box<ObjectError>),
 
     /// The `Object`s architecture is not supported by symbolic.
-    #[fail(display = "unsupported architecture")]
-    UnsupportedArch,
+    #[error("unsupported architecture")]
+    UnsupportedArch(#[from] UnknownArchError),
 
     /// CFI for an invalid address outside the mapped range was encountered.
-    #[fail(display = "invalid cfi address")]
+    #[error("invalid cfi address")]
     InvalidAddress,
 
     /// Generic error when writing CFI information, likely IO.
-    #[fail(display = "failed to write cfi")]
-    WriteError,
+    #[error("failed to write cfi")]
+    WriteError(#[from] io::Error),
 
     /// Invalid magic bytes in the cfi cache header.
-    #[fail(display = "bad cfi cache magic")]
+    #[error("bad cfi cache magic")]
     BadFileMagic,
 }
 
-derive_failure!(
-    CfiError,
-    CfiErrorKind,
-    doc = "An error returned by [`AsciiCfiWriter`](struct.AsciiCfiWriter.html)."
-);
+impl From<BreakpadError> for CfiError {
+    fn from(e: BreakpadError) -> Self {
+        Box::new(ObjectError::from(e)).into()
+    }
+}
+
+impl From<pdb::Error> for CfiError {
+    fn from(e: pdb::Error) -> Self {
+        Box::new(ObjectError::from(PdbError::from(e))).into()
+    }
+}
+
+impl From<GoblinError> for CfiError {
+    fn from(e: GoblinError) -> Self {
+        Box::new(ObjectError::from(ElfError::from(e))).into()
+    }
+}
 
-impl From<UnknownArchError> for CfiError {
-    fn from(_: UnknownArchError) -> CfiError {
-        CfiErrorKind::UnsupportedArch.into()
+impl From<GimliError> for CfiError {
+    fn from(e: GimliError) -> Self {
+        Box::new(ObjectError::from(DwarfError::from(e))).into()
     }
 }
 
@@ -160,7 +172,7 @@ impl<U> UnwindInfo<U> {
 /// use symbolic_debuginfo::Object;
 /// use symbolic_minidump::cfi::AsciiCfiWriter;
 ///
-/// # fn main() -> Result<(), failure::Error> {
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let view = ByteView::open("/path/to/object")?;
 /// let object = Object::parse(&view)?;
 ///
@@ -178,7 +190,7 @@ impl<U> UnwindInfo<U> {
 /// use symbolic_debuginfo::Object;
 /// use symbolic_minidump::cfi::AsciiCfiWriter;
 ///
-/// # fn main() -> Result<(), failure::Error> {
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let view = ByteView::open("/path/to/object")?;
 /// let object = Object::parse(&view)?;
 ///
@@ -215,11 +227,10 @@ impl<W: Write> AsciiCfiWriter<W> {
 
     fn process_breakpad(&mut self, object: &BreakpadObject<'_>) -> Result<(), CfiError> {
         for record in object.stack_records() {
-            match record.context(CfiErrorKind::BadDebugInfo)? {
+            match record? {
                 BreakpadStackRecord::Cfi(r) => writeln!(self.inner, "STACK CFI {}", r.text),
                 BreakpadStackRecord::Win(r) => writeln!(self.inner, "STACK WIN {}", r.text),
-            }
-            .context(CfiErrorKind::WriteError)?
+            }?
         }
 
         Ok(())
@@ -261,7 +272,7 @@ impl<W: Write> AsciiCfiWriter<W> {
         let mut ctx = UninitializedUnwindContext::new();
 
         let mut entries = info.section.entries(&info.bases);
-        while let Some(entry) = entries.next().context(CfiErrorKind::BadDebugInfo)? {
+        while let Some(entry) = entries.next()? {
             // We skip all Common Information Entries and only process Frame Description Items here.
             // The iterator yields partial FDEs which need their associated CIE passed in via a
             // callback. This function is provided by the UnwindSection (frame), which then parses
@@ -293,9 +304,7 @@ impl<W: Write> AsciiCfiWriter<W> {
         // Interpret all DWARF instructions of this Frame Description Entry. This gives us an unwind
         // table that contains rules for retrieving registers at every instruction address. These
         // rules can directly be transcribed to breakpad STACK CFI records.
-        let mut table = fde
-            .rows(&info.section, &info.bases, ctx)
-            .context(CfiErrorKind::BadDebugInfo)?;
+        let mut table = fde.rows(&info.section, &info.bases, ctx)?;
 
         // Collect all rows first, as we need to know the final end address in order to write the
         // CFI INIT record describing the extent of the whole unwind table.
@@ -307,7 +316,7 @@ impl<W: Write> AsciiCfiWriter<W> {
                 Err(Error::UnknownCallFrameInstruction(_)) => continue,
                 // NOTE: Temporary workaround for https://github.com/gimli-rs/gimli/pull/487
                 Err(Error::TooManyRegisterRules) => continue,
-                Err(e) => return Err(e.context(CfiErrorKind::BadDebugInfo).into()),
+                Err(e) => return Err(e.into()),
             }
         }
 
@@ -338,11 +347,10 @@ impl<W: Write> AsciiCfiWriter<W> {
                 // normal STACK CFI record.
                 if row.start_address() == start {
                     let start_addr = start - info.load_address;
-                    write!(line, "STACK CFI INIT {:x} {:x}", start_addr, length)
-                        .context(CfiErrorKind::WriteError)?;
+                    write!(line, "STACK CFI INIT {:x} {:x}", start_addr, length)?;
                 } else {
                     let start_addr = row.start_address() - info.load_address;
-                    write!(line, "STACK CFI {:x}", start_addr).context(CfiErrorKind::WriteError)?;
+                    write!(line, "STACK CFI {:x}", start_addr)?;
                 }
 
                 // Write the mandatory CFA rule for this row, followed by optional register rules.
@@ -366,8 +374,7 @@ impl<W: Write> AsciiCfiWriter<W> {
                 if written {
                     self.inner
                         .write_all(&line)
-                        .and_then(|_| writeln!(self.inner))
-                        .context(CfiErrorKind::WriteError)?;
+                        .and_then(|_| writeln!(self.inner))?;
                 }
             }
         }
@@ -390,7 +397,7 @@ impl<W: Write> AsciiCfiWriter<W> {
             CfaRule::Expression(_) => return Ok(false),
         };
 
-        write!(target, " .cfa: {}", formatted).context(CfiErrorKind::WriteError)?;
+        write!(target, " .cfa: {}", formatted)?;
         Ok(true)
     }
 
@@ -431,26 +438,26 @@ impl<W: Write> AsciiCfiWriter<W> {
             }
         };
 
-        write!(target, " {}: {}", register_name, formatted).context(CfiErrorKind::WriteError)?;
+        write!(target, " {}: {}", register_name, formatted)?;
         Ok(true)
     }
 
     fn process_pdb(&mut self, pdb: &PdbObject<'_>) -> Result<(), CfiError> {
         let mut pdb = pdb.inner().write();
-        let frame_table = pdb.frame_table().context(CfiErrorKind::BadDebugInfo)?;
-        let address_map = pdb.address_map().context(CfiErrorKind::BadDebugInfo)?;
+        let frame_table = pdb.frame_table()?;
+        let address_map = pdb.address_map()?;
 
         // See `PdbDebugSession::build`.
         let string_table = match pdb.string_table() {
             Ok(string_table) => Some(string_table),
             Err(pdb::Error::StreamNameNotFound) => None,
-            Err(e) => Err(e).context(CfiErrorKind::BadDebugInfo)?,
+            Err(e) => return Err(e.into()),
         };
 
         let mut frames = frame_table.iter();
         let mut last_frame: Option<FrameData> = None;
 
-        while let Some(frame) = frames.next().context(CfiErrorKind::BadDebugInfo)? {
+        while let Some(frame) = frames.next()? {
             // Frame data information sometimes contains code_size values close to the maximum `u32`
             // value, such as `0xffffff6e`. Documentation does not describe the meaning of such
             // values, but clearly they are not actual code sizes. Since these values also always
@@ -557,26 +564,21 @@ impl<W: Write> AsciiCfiWriter<W> {
             frame.locals_size,
             frame.max_stack_size.unwrap_or(0),
             if program_or_bp { 1 } else { 0 },
-        )
-        .context(CfiErrorKind::WriteError)?;
+        )?;
 
         match frame.program {
             Some(ref prog_ref) => {
                 let string_table = match string_table {
                     Some(string_table) => string_table,
-                    None => return Ok(writeln!(self.inner).context(CfiErrorKind::WriteError)?),
+                    None => return Ok(writeln!(self.inner)?),
                 };
 
-                let program_string = prog_ref
-                    .to_string_lossy(&string_table)
-                    .context(CfiErrorKind::BadDebugInfo)?;
+                let program_string = prog_ref.to_string_lossy(&string_table)?;
 
-                writeln!(self.inner, "{}", program_string.trim())
-                    .context(CfiErrorKind::WriteError)?;
+                writeln!(self.inner, "{}", program_string.trim())?;
             }
             None => {
-                writeln!(self.inner, "{}", if program_or_bp { 1 } else { 0 })
-                    .context(CfiErrorKind::WriteError)?;
+                writeln!(self.inner, "{}", if program_or_bp { 1 } else { 0 })?;
             }
         }
 
@@ -591,7 +593,7 @@ impl<W: Write> AsciiCfiWriter<W> {
         };
 
         for function_result in exception_data {
-            let function = function_result.context(CfiErrorKind::BadDebugInfo)?;
+            let function = function_result?;
 
             // Exception directories can contain zeroed out sections which need to be skipped.
             // Neither their start/end RVA nor the unwind info RVA is valid.
@@ -610,9 +612,7 @@ impl<W: Write> AsciiCfiWriter<W> {
 
             let mut next_function = Some(function);
             while let Some(next) = next_function {
-                let unwind_info = exception_data
-                    .get_unwind_info(next, sections)
-                    .context(CfiErrorKind::BadDebugInfo)?;
+                let unwind_info = exception_data.get_unwind_info(next, sections)?;
 
                 for code_result in &unwind_info {
                     // Due to variable length encoding of operator codes, there is little point in
@@ -648,8 +648,7 @@ impl<W: Write> AsciiCfiWriter<W> {
                 "STACK CFI INIT {:x} {:x} .cfa: $rsp 8 + .ra: .cfa 8 - ^",
                 function.begin_address,
                 function.end_address - function.begin_address,
-            )
-            .context(CfiErrorKind::WriteError)?;
+            )?;
 
             if machine_frame_offset > 0 {
                 writeln!(
@@ -659,15 +658,13 @@ impl<W: Write> AsciiCfiWriter<W> {
                     stack_size,
                     stack_size - machine_frame_offset + 24, // old RSP offset
                     stack_size - machine_frame_offset + 48, // entire frame offset
-                )
-                .context(CfiErrorKind::WriteError)?
+                )?
             } else {
                 writeln!(
                     self.inner,
                     "STACK CFI {:x} .cfa: $rsp {} +",
                     function.begin_address, stack_size,
-                )
-                .context(CfiErrorKind::WriteError)?
+                )?
             }
         }
 
@@ -709,7 +706,7 @@ enum CfiCacheInner<'a> {
 /// use symbolic_debuginfo::Object;
 /// use symbolic_minidump::cfi::CfiCache;
 ///
-/// # fn main() -> Result<(), failure::Error> {
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let view = ByteView::open("/path/to/object")?;
 /// let object = Object::parse(&view)?;
 /// let cache = CfiCache::from_object(&object)?;
@@ -722,7 +719,7 @@ enum CfiCacheInner<'a> {
 /// use symbolic_common::ByteView;
 /// use symbolic_minidump::cfi::CfiCache;
 ///
-/// # fn main() -> Result<(), failure::Error> {
+/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
 /// let view = ByteView::open("my.cficache")?;
 /// let cache = CfiCache::from_bytes(view)?;
 /// # Ok(())
@@ -751,7 +748,7 @@ impl<'a> CfiCache<'a> {
             return Ok(CfiCache { inner });
         }
 
-        Err(CfiErrorKind::BadFileMagic.into())
+        Err(CfiError::BadFileMagic)
     }
 
     /// Returns the cache file format version.
diff --git a/symbolic-minidump/src/processor.rs b/symbolic-minidump/src/processor.rs
index f6b4ec1e..fcaf4b2b 100644
--- a/symbolic-minidump/src/processor.rs
+++ b/symbolic-minidump/src/processor.rs
@@ -18,7 +18,6 @@ use std::os::raw::{c_char, c_void};
 use std::str::FromStr;
 use std::{fmt, ptr, slice, str};
 
-use failure::Fail;
 use lazy_static::lazy_static;
 use regex::Regex;
 
@@ -365,8 +364,10 @@ impl fmt::Display for FrameTrust {
     }
 }
 
-/// Error when converting a string to FrameTrust.
-#[derive(Clone, Copy, Debug)]
+/// Error when converting a string to [`FrameTrust`].
+///
+/// [`FrameTrust`]: enum.FrameTrust.html
+#[derive(Debug)]
 pub struct ParseFrameTrustError;
 
 impl fmt::Display for ParseFrameTrustError {
@@ -392,6 +393,8 @@ impl FromStr for FrameTrust {
     }
 }
 
+impl std::error::Error for ParseFrameTrustError {}
+
 impl Default for FrameTrust {
     fn default() -> FrameTrust {
         FrameTrust::None
@@ -758,18 +761,25 @@ impl fmt::Display for ProcessResult {
 }
 
 /// An error generated when trying to process a minidump.
-#[derive(Debug, Fail, Copy, Clone)]
+#[derive(Debug)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[fail(display = "minidump processing failed: {}", _0)]
 pub struct ProcessMinidumpError(ProcessResult);
 
 impl ProcessMinidumpError {
     /// Returns the kind of this error.
-    pub fn kind(self) -> ProcessResult {
+    pub fn kind(&self) -> ProcessResult {
         self.0
     }
 }
 
+impl fmt::Display for ProcessMinidumpError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "minidump processing failed: {}", self.0)
+    }
+}
+
+impl std::error::Error for ProcessMinidumpError {}
+
 /// Internal type used to transfer Breakpad symbols over FFI.
 #[repr(C)]
 struct SymbolEntry {
diff --git a/symbolic-proguard/Cargo.toml b/symbolic-proguard/Cargo.toml
deleted file mode 100644
index a7e93ff4..00000000
--- a/symbolic-proguard/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "symbolic-proguard"
-version = "7.5.0"
-license = "MIT"
-authors = [
-    "Armin Ronacher <armin.ronacher@active-4.com>",
-    "Jan Michael Auer <mail@jauer.org>",
-]
-documentation = "https://docs.rs/symbolic-proguard"
-keywords = ["proguard", "retrace", "R8"]
-homepage = "https://github.com/getsentry/symbolic"
-repository = "https://github.com/getsentry/symbolic"
-description = """
-A library to process ProGuard and R8 mapping files and symbolicate frames from
-optimized applications.
-"""
-edition = "2018"
-
-[package.metadata.docs.rs]
-all-features = true
-
-[dependencies]
-proguard = { version = "4.0.1", features = ["uuid"] }
-symbolic-common = { version = "7.5.0", path = "../symbolic-common" }
diff --git a/symbolic-proguard/src/lib.rs b/symbolic-proguard/src/lib.rs
deleted file mode 100644
index 4ab654f5..00000000
--- a/symbolic-proguard/src/lib.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-//! Provides proguard support.
-
-#![warn(missing_docs)]
-
-use std::io;
-
-use proguard::{ProguardMapper, ProguardMapping, StackFrame};
-
-use symbolic_common::{AsSelf, ByteView, SelfCell, Uuid};
-
-struct Inner<'a> {
-    mapping: ProguardMapping<'a>,
-    mapper: ProguardMapper<'a>,
-}
-
-impl<'slf, 'a: 'slf> AsSelf<'slf> for Inner<'a> {
-    type Ref = Inner<'slf>;
-
-    fn as_self(&'slf self) -> &Self::Ref {
-        &self
-    }
-}
-
-/// A view over a proguard mapping text file.
-#[deprecated = "use the `proguard` crate directly"]
-pub struct ProguardMappingView<'a> {
-    inner: SelfCell<ByteView<'a>, Inner<'a>>,
-}
-
-#[allow(deprecated)]
-impl<'a> ProguardMappingView<'a> {
-    /// Creates a new proguard mapping view from a byte slice.
-    pub fn parse(byteview: ByteView<'a>) -> Result<Self, io::Error> {
-        let inner = SelfCell::new(byteview, |data| {
-            let mapping = ProguardMapping::new(unsafe { &*data });
-            let mapper = ProguardMapper::new(mapping.clone());
-            Inner { mapping, mapper }
-        });
-
-        Ok(ProguardMappingView { inner })
-    }
-
-    /// Returns the mapping UUID.
-    pub fn uuid(&self) -> Uuid {
-        self.inner.get().mapping.uuid()
-    }
-
-    /// Returns true if this file has line infos.
-    pub fn has_line_info(&self) -> bool {
-        self.inner.get().mapping.has_line_info()
-    }
-
-    /// Converts a dotted path.
-    pub fn convert_dotted_path(&self, path: &str, lineno: u32) -> String {
-        let mapper = &self.inner.get().mapper;
-
-        let mut iter = path.splitn(2, ':');
-        let cls_name = iter.next().unwrap_or("");
-        match iter.next() {
-            Some(meth_name) => {
-                let mut mapped =
-                    mapper.remap_frame(&StackFrame::new(cls_name, meth_name, lineno as usize));
-                match mapped.next() {
-                    Some(frame) => format!("{}:{}", frame.class(), frame.method()),
-                    None => path.to_string(),
-                }
-            }
-            None => mapper.remap_class(cls_name).unwrap_or(cls_name).to_string(),
-        }
-    }
-}
diff --git a/symbolic-sourcemap/Cargo.toml b/symbolic-sourcemap/Cargo.toml
index 426c1bf6..d805bb15 100644
--- a/symbolic-sourcemap/Cargo.toml
+++ b/symbolic-sourcemap/Cargo.toml
@@ -19,5 +19,4 @@ edition = "2018"
 all-features = true
 
 [dependencies]
-failure = "0.1.5"
 sourcemap = "5.0.0"
diff --git a/symbolic-sourcemap/src/lib.rs b/symbolic-sourcemap/src/lib.rs
index 13930aac..e535aebe 100644
--- a/symbolic-sourcemap/src/lib.rs
+++ b/symbolic-sourcemap/src/lib.rs
@@ -6,8 +6,6 @@ use std::borrow::Cow;
 use std::fmt;
 use std::ops::Deref;
 
-use failure::Fail;
-
 /// An error returned when parsing source maps.
 #[derive(Debug)]
 pub struct ParseSourceMapError(sourcemap::Error);
@@ -23,8 +21,8 @@ impl fmt::Display for ParseSourceMapError {
     }
 }
 
-impl Fail for ParseSourceMapError {
-    fn cause(&self) -> Option<&dyn Fail> {
+impl std::error::Error for ParseSourceMapError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         Some(match self.0 {
             sourcemap::Error::Io(ref err) => err,
             sourcemap::Error::Utf8(ref err) => err,
diff --git a/symbolic-symcache/Cargo.toml b/symbolic-symcache/Cargo.toml
index 9578fac3..9b871da6 100644
--- a/symbolic-symcache/Cargo.toml
+++ b/symbolic-symcache/Cargo.toml
@@ -24,11 +24,11 @@ all-features = true
 
 [dependencies]
 dmsort = "1.0.0"
-failure = "0.1.5"
 fnv = "1.0.6"
 num = "0.2.1"
 symbolic-common = { version = "7.5.0", path = "../symbolic-common" }
 symbolic-debuginfo = { version = "7.5.0", path = "../symbolic-debuginfo" }
+thiserror = "1.0.20"
 
 [dev-dependencies]
 insta = "0.15.0"
diff --git a/symbolic-symcache/src/cache.rs b/symbolic-symcache/src/cache.rs
index 1846437f..ad64e655 100644
--- a/symbolic-symcache/src/cache.rs
+++ b/symbolic-symcache/src/cache.rs
@@ -2,7 +2,7 @@ use std::fmt;
 
 use symbolic_common::{Arch, AsSelf, DebugId, Language, Name};
 
-use crate::error::{SymCacheError, SymCacheErrorKind};
+use crate::error::SymCacheError;
 use crate::format;
 
 /// A platform independent symbolication cache.
@@ -267,7 +267,7 @@ impl<'a> SymCache<'a> {
             if let Some((line_addr, file_id, line)) = self.run_to_line(fun, addr)? {
                 // A missing file record indicates a bad symcache.
                 let file_record = read_file_record(self.data, self.header.files, file_id)?
-                    .ok_or_else(|| SymCacheErrorKind::BadCacheFile)?;
+                    .ok_or_else(|| SymCacheError::BadCacheFile)?;
 
                 // The address was found in the function's line records, so use
                 // it directly. This should is the default case for all valid
diff --git a/symbolic-symcache/src/error.rs b/symbolic-symcache/src/error.rs
index c5f4dc42..bed52921 100644
--- a/symbolic-symcache/src/error.rs
+++ b/symbolic-symcache/src/error.rs
@@ -1,9 +1,6 @@
 use std::fmt;
 
-use failure::Fail;
-
-use symbolic_common::derive_failure;
-use symbolic_debuginfo::ObjectError;
+use thiserror::Error;
 
 #[doc(hidden)]
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -29,67 +26,55 @@ impl fmt::Display for ValueKind {
     }
 }
 
-/// Variants of `SymCacheError`.
+/// An error returned when handling [`SymCache`](struct.SymCache.html).
 #[non_exhaustive]
-#[derive(Clone, Copy, Debug, Eq, Fail, PartialEq)]
-pub enum SymCacheErrorKind {
+#[derive(Debug, Error)]
+pub enum SymCacheError {
     /// Invalid magic bytes in the symcache header.
-    #[fail(display = "bad symcache magic")]
+    #[error("bad symcache magic")]
     BadFileMagic,
 
     /// Invalid flags or fields in the symcache header.
-    #[fail(display = "invalid symcache header")]
-    BadFileHeader,
+    #[error("invalid symcache header")]
+    BadFileHeader(#[source] std::io::Error),
 
     /// A segment could not be read, likely due to IO errors.
-    #[fail(display = "cannot read symcache segment")]
+    #[error("cannot read symcache segment")]
     BadSegment,
 
     /// Contents in the symcache file are malformed.
-    #[fail(display = "malformed symcache file")]
+    #[error("malformed symcache file")]
     BadCacheFile,
 
     /// The symcache version is not known.
-    #[fail(display = "unsupported symcache version")]
+    #[error("unsupported symcache version")]
     UnsupportedVersion,
 
     /// The `Object` contains invalid data and cannot be converted.
-    #[fail(display = "malformed debug info file")]
-    BadDebugFile,
+    #[error("malformed debug info file")]
+    BadDebugFile(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
 
     /// A required debug section is missing in the `Object` file.
-    #[fail(display = "missing debug section")]
+    #[error("missing debug section")]
     MissingDebugSection,
 
     /// The `Object` file was stripped of debug information.
-    #[fail(display = "no debug information found in file")]
+    #[error("no debug information found in file")]
     MissingDebugInfo,
 
     /// The debug information in the `Object` file is not supported.
-    #[fail(display = "unsupported debug information")]
+    #[error("unsupported debug information")]
     UnsupportedDebugKind,
 
     /// A value cannot be written to symcache as it overflows the record size.
-    #[fail(display = "{} too large for symcache file format", _0)]
+    #[error("{0} too large for symcache file format")]
     ValueTooLarge(ValueKind),
 
     /// A value cannot be written to symcache as it overflows the segment counter.
-    #[fail(display = "too many {}s for symcache", _0)]
+    #[error("too many {0}s for symcache")]
     TooManyValues(ValueKind),
 
     /// Generic error when writing a symcache, most likely IO.
-    #[fail(display = "failed to write symcache")]
-    WriteFailed,
-}
-
-derive_failure!(
-    SymCacheError,
-    SymCacheErrorKind,
-    doc = "An error returned when handling `SymCaches`.",
-);
-
-impl From<ObjectError> for SymCacheError {
-    fn from(error: ObjectError) -> SymCacheError {
-        error.context(SymCacheErrorKind::BadDebugFile).into()
-    }
+    #[error("failed to write symcache")]
+    WriteFailed(#[source] std::io::Error),
 }
diff --git a/symbolic-symcache/src/format.rs b/symbolic-symcache/src/format.rs
index 82d9d45e..97d04063 100644
--- a/symbolic-symcache/src/format.rs
+++ b/symbolic-symcache/src/format.rs
@@ -5,11 +5,9 @@ use std::fmt;
 use std::io;
 use std::marker::PhantomData;
 
-use failure::ResultExt;
-
 use symbolic_common::{DebugId, Uuid};
 
-use crate::error::{SymCacheError, SymCacheErrorKind};
+use crate::error::SymCacheError;
 
 /// The magic file preamble to identify symcache files.
 pub const SYMCACHE_MAGIC: [u8; 4] = *b"SYMC";
@@ -82,7 +80,7 @@ where
         let offset = self.offset as usize;
         let len = self.len.into() as usize;
         let size = std::mem::size_of::<T>() * len;
-        let slice = get_slice(data, offset, size).context(SymCacheErrorKind::BadSegment)?;
+        let slice = get_slice(data, offset, size).map_err(|_| SymCacheError::BadSegment)?;
         Ok(unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const T, len) })
     }
 
@@ -102,7 +100,7 @@ where
     /// Reads an entire binary segment as string.
     pub fn read_str<'a>(&self, data: &'a [u8]) -> Result<&'a str, SymCacheError> {
         let slice = self.read(data)?;
-        Ok(std::str::from_utf8(slice).context(SymCacheErrorKind::BadSegment)?)
+        Ok(std::str::from_utf8(slice).map_err(|_| SymCacheError::BadSegment)?)
     }
 }
 
@@ -321,20 +319,20 @@ pub struct Header {
 impl Header {
     /// Parses the correct version of the SymCache [`Header`](struct.Header.html).
     pub fn parse(data: &[u8]) -> Result<Self, SymCacheError> {
-        let preamble = get_record::<Preamble>(data, 0).context(SymCacheErrorKind::BadFileHeader)?;
+        let preamble = get_record::<Preamble>(data, 0).map_err(SymCacheError::BadFileHeader)?;
 
         if preamble.magic != SYMCACHE_MAGIC {
-            return Err(SymCacheErrorKind::BadFileMagic.into());
+            return Err(SymCacheError::BadFileMagic);
         }
 
         Ok(match preamble.version {
             1 => get_record::<HeaderV1>(data, 0)
-                .context(SymCacheErrorKind::BadFileHeader)?
+                .map_err(SymCacheError::BadFileHeader)?
                 .into(),
             2..=SYMCACHE_VERSION => get_record::<HeaderV2>(data, 0)
-                .context(SymCacheErrorKind::BadFileHeader)?
+                .map_err(SymCacheError::BadFileHeader)?
                 .into(),
-            _ => return Err(SymCacheErrorKind::UnsupportedVersion.into()),
+            _ => return Err(SymCacheError::UnsupportedVersion),
         })
     }
 }
diff --git a/symbolic-symcache/src/writer.rs b/symbolic-symcache/src/writer.rs
index a0fbde05..b3abd519 100644
--- a/symbolic-symcache/src/writer.rs
+++ b/symbolic-symcache/src/writer.rs
@@ -2,14 +2,13 @@ use std::borrow::Cow;
 use std::collections::HashMap;
 use std::io::{self, Seek, Write};
 
-use failure::{Fail, ResultExt};
 use fnv::{FnvHashMap, FnvHashSet};
 use num::FromPrimitive;
 
 use symbolic_common::{Arch, DebugId, Language};
 use symbolic_debuginfo::{DebugSession, FileInfo, Function, LineInfo, ObjectLike, Symbol};
 
-use crate::error::{SymCacheError, SymCacheErrorKind, ValueKind};
+use crate::error::{SymCacheError, ValueKind};
 use crate::format;
 
 // Performs a shallow check whether this function might contain any lines.
@@ -69,7 +68,7 @@ where
         self.position = position;
         self.writer
             .seek(io::SeekFrom::Start(position))
-            .context(SymCacheErrorKind::WriteFailed)?;
+            .map_err(SymCacheError::WriteFailed)?;
 
         Ok(())
     }
@@ -79,7 +78,7 @@ where
     fn write_bytes(&mut self, data: &[u8]) -> Result<(), SymCacheError> {
         self.writer
             .write_all(data)
-            .context(SymCacheErrorKind::WriteFailed)?;
+            .map_err(SymCacheError::WriteFailed)?;
 
         self.position += data.len() as u64;
         Ok(())
@@ -112,7 +111,7 @@ where
 
         let segment_pos = self.position as u32;
         let segment_len =
-            L::from_usize(data.len()).ok_or_else(|| SymCacheErrorKind::TooManyValues(kind))?;
+            L::from_usize(data.len()).ok_or_else(|| SymCacheError::TooManyValues(kind))?;
 
         self.write_bytes(bytes)?;
         Ok(format::Seg::new(segment_pos, segment_len))
@@ -198,19 +197,19 @@ where
     pub fn write_object<O>(object: &O, target: W) -> Result<W, SymCacheError>
     where
         O: ObjectLike,
-        O::Error: Fail,
+        O::Error: std::error::Error + Send + Sync + 'static,
     {
-        let mut writer = SymCacheWriter::new(target).context(SymCacheErrorKind::WriteFailed)?;
+        let mut writer = SymCacheWriter::new(target)?;
 
         writer.set_arch(object.arch());
         writer.set_debug_id(object.debug_id());
 
         let session = object
             .debug_session()
-            .context(SymCacheErrorKind::BadDebugFile)?;
+            .map_err(|e| SymCacheError::BadDebugFile(Box::new(e)))?;
 
         for function in session.functions() {
-            let function = function.context(SymCacheErrorKind::BadDebugFile)?;
+            let function = function.map_err(|e| SymCacheError::BadDebugFile(Box::new(e)))?;
             writer.add_function(function)?;
         }
 
@@ -374,7 +373,7 @@ where
         }
 
         if self.files.len() >= std::u16::MAX as usize {
-            return Err(SymCacheErrorKind::TooManyValues(ValueKind::File).into());
+            return Err(SymCacheError::TooManyValues(ValueKind::File));
         }
 
         let index = self.files.len() as u16;
@@ -398,7 +397,7 @@ where
 
         // NB: We only use 48 bits to encode symbol offsets in function records.
         if self.symbols.len() >= 0x00ff_ffff {
-            return Err(SymCacheErrorKind::TooManyValues(ValueKind::Symbol).into());
+            return Err(SymCacheError::TooManyValues(ValueKind::Symbol));
         }
 
         // Avoid a potential reallocation by reusing name.
@@ -477,7 +476,7 @@ where
         let symbol_id = self.insert_symbol(function.name.as_str().into())?;
         let comp_dir = self.write_path(function.compilation_dir)?;
         let lang = u8::from_u32(language as u32)
-            .ok_or_else(|| SymCacheErrorKind::ValueTooLarge(ValueKind::Language))?;
+            .ok_or_else(|| SymCacheError::ValueTooLarge(ValueKind::Language))?;
 
         let mut start_address = function.address;
         let mut lines = function.lines.iter().peekable();
@@ -534,7 +533,7 @@ where
         // functions to the file.
         let index = functions.len();
         if index >= std::u32::MAX as usize {
-            return Err(SymCacheErrorKind::ValueTooLarge(ValueKind::Function).into());
+            return Err(SymCacheError::ValueTooLarge(ValueKind::Function));
         }
 
         // For optimization purposes, remember if all functions appear in order. If not, parent
@@ -602,7 +601,7 @@ where
 
                 let parent_offset = index - parent_index;
                 if parent_offset > std::u16::MAX.into() {
-                    return Err(SymCacheErrorKind::ValueTooLarge(ValueKind::ParentOffset).into());
+                    return Err(SymCacheError::ValueTooLarge(ValueKind::ParentOffset));
                 }
 
                 record.parent_offset = parent_offset as u16;
diff --git a/symbolic-unreal/Cargo.toml b/symbolic-unreal/Cargo.toml
index cad0f6b5..7b9caa23 100644
--- a/symbolic-unreal/Cargo.toml
+++ b/symbolic-unreal/Cargo.toml
@@ -30,12 +30,12 @@ anylog = "0.5.0"
 bytes = "0.4.12"
 chrono = "0.4.7"
 elementtree = "0.5.0"
-failure = "0.1.5"
 lazy_static = "1.4.0"
 flate2 = { version = "1.0.13", features = ["rust_backend"], default-features = false }
 regex = "1.3.5"
 scroll = { version = "0.10.1", features = ["derive"] }
 serde_ = { package = "serde", version = "1.0.94", optional = true, features = ["derive"] }
+thiserror = "1.0.20"
 
 [dev-dependencies]
 insta = "0.15.0"
diff --git a/symbolic-unreal/src/context.rs b/symbolic-unreal/src/context.rs
index f8817cdd..8dedc292 100644
--- a/symbolic-unreal/src/context.rs
+++ b/symbolic-unreal/src/context.rs
@@ -352,7 +352,7 @@ fn load_data_bag(element: &Element) -> BTreeMap<String, String> {
 impl Unreal4Context {
     /// Parses the unreal context XML file.
     pub fn parse(data: &[u8]) -> Result<Self, Unreal4Error> {
-        let root = Element::from_reader(data).map_err(Unreal4Error::InvalidXml)?;
+        let root = Element::from_reader(data)?;
 
         Ok(Unreal4Context {
             runtime_properties: Unreal4ContextRuntimeProperties::from_xml(&root),
diff --git a/symbolic-unreal/src/error.rs b/symbolic-unreal/src/error.rs
index 67a480c0..d0783cf2 100644
--- a/symbolic-unreal/src/error.rs
+++ b/symbolic-unreal/src/error.rs
@@ -1,36 +1,30 @@
-use failure::Fail;
+use thiserror::Error;
 
 /// Errors related to parsing an UE4 crash file.
 #[non_exhaustive]
-#[derive(Fail, Debug)]
+#[derive(Error, Debug)]
 pub enum Unreal4Error {
     /// Empty data blob received.
-    #[fail(display = "empty crash")]
+    #[error("empty crash")]
     Empty,
 
     /// Invalid compressed data.
-    #[fail(display = "bad compression")]
-    BadCompression(std::io::Error),
+    #[error("bad compression")]
+    BadCompression(#[source] std::io::Error),
 
     /// Invalid contents of the crash file container.
-    #[fail(display = "invalid crash file contents")]
-    BadData(scroll::Error),
+    #[error("invalid crash file contents")]
+    BadData(#[from] scroll::Error),
 
     /// The crash file contains unexpected trailing data after the footer.
-    #[fail(display = "unexpected trailing data")]
+    #[error("unexpected trailing data")]
     TrailingData,
 
     /// Can't process a log entry.
-    #[fail(display = "invalid log entry")]
-    InvalidLogEntry(std::str::Utf8Error),
+    #[error("invalid log entry")]
+    InvalidLogEntry(#[source] std::str::Utf8Error),
 
     /// Invalid XML.
-    #[fail(display = "invalid xml")]
-    InvalidXml(elementtree::Error),
-}
-
-impl From<scroll::Error> for Unreal4Error {
-    fn from(error: scroll::Error) -> Self {
-        Unreal4Error::BadData(error)
-    }
+    #[error("invalid xml")]
+    InvalidXml(#[from] elementtree::Error),
 }
diff --git a/symbolic/Cargo.toml b/symbolic/Cargo.toml
index 2e601d19..13a8a18c 100644
--- a/symbolic/Cargo.toml
+++ b/symbolic/Cargo.toml
@@ -27,7 +27,6 @@ debuginfo-serde = ["debuginfo", "common-serde"]
 demangle = ["symbolic-demangle"]
 minidump = ["symbolic-minidump", "debuginfo"]
 minidump-serde = ["minidump", "debuginfo-serde", "symbolic-minidump/serde"]
-proguard = ["symbolic-proguard"]
 sourcemap = ["symbolic-sourcemap"]
 symcache = ["symbolic-symcache", "debuginfo"]
 unreal = ["symbolic-unreal"]
@@ -38,7 +37,6 @@ symbolic-common = { version = "7.5.0", path = "../symbolic-common" }
 symbolic-debuginfo = { version = "7.5.0", path = "../symbolic-debuginfo", optional = true }
 symbolic-demangle = { version = "7.5.0", path = "../symbolic-demangle", optional = true }
 symbolic-minidump = { version = "7.5.0", path = "../symbolic-minidump", optional = true }
-symbolic-proguard = { version = "7.5.0", path = "../symbolic-proguard", optional = true }
 symbolic-sourcemap = { version = "7.5.0", path = "../symbolic-sourcemap", optional = true }
 symbolic-symcache = { version = "7.5.0", path = "../symbolic-symcache", optional = true }
 symbolic-unreal = { version = "7.5.0", path = "../symbolic-unreal", optional = true }
diff --git a/symbolic/src/lib.rs b/symbolic/src/lib.rs
index 0943f884..9b1dcf86 100644
--- a/symbolic/src/lib.rs
+++ b/symbolic/src/lib.rs
@@ -22,7 +22,7 @@
 //!   - Basic token mapping
 //!   - Heuristics to find original function names based on minified sources
 //!   - Indexed sourcemap to sourcemap merging
-//! - Proguard function mappings
+//! - Proguard function mappings (via `symbolic-cabi` only, use `proguard` crate instead)
 //! - Minidump / Breakpad processing
 //!   - Generate Breakpad symbol files from Mach, ELF and PDBs
 //!   - Process Minidumps to retrieve stack traces
@@ -44,7 +44,6 @@
 //! - **`minidump`**: Rust bindings for the Breakpad Minidump processor. Additionally, this includes
 //!   facilities to extract stack unwinding information (sometimes called CFI) from object files.
 //!   This feature requires a C++11 compiler on the PATH.
-//! - **`proguard`**: Processing of Proguard mapping files to look up mangled Java function paths.
 //! - **`sourcemap`**: Processing and expansion of JavaScript source maps, as well as lookups for
 //!   minified function names.
 //! - **`symcache`**: An optimized, platform-independent storage for common debugging information.
@@ -74,10 +73,6 @@ pub use symbolic_demangle as demangle;
 #[cfg(feature = "minidump")]
 pub use symbolic_minidump as minidump;
 #[doc(inline)]
-#[cfg(feature = "proguard")]
-#[deprecated = "use the `proguard` crate directly"]
-pub use symbolic_proguard as proguard;
-#[doc(inline)]
 #[cfg(feature = "sourcemap")]
 pub use symbolic_sourcemap as sourcemap;
 #[doc(inline)]


Test patch:
diff --git a/py/tests/test_proguard.py b/py/tests/test_proguard.py
index 8b5b9c01..eebf0af8 100644
--- a/py/tests/test_proguard.py
+++ b/py/tests/test_proguard.py
@@ -1,41 +1,6 @@
 import os
 import uuid
-from symbolic import ProguardMappingView, ProguardMapper
-
-
-def test_basics(res_path):
-    with open(os.path.join(res_path, "proguard.txt"), "rb") as f:
-        mapping = f.read()
-
-    view = ProguardMappingView.from_bytes(mapping)
-    assert view.has_line_info
-    assert view.uuid == uuid.UUID("a48ca62b-df26-544e-a8b9-2a5ce210d1d5")
-
-    assert (
-        view.lookup("android.support.constraint.ConstraintLayout$a")
-        == "android.support.constraint.ConstraintLayout$LayoutParams"
-    )
-
-    assert (
-        view.lookup("android.support.constraint.a.b:a", 116)
-        == "android.support.constraint.solver.ArrayRow:createRowDefinition"
-    )
-
-
-def test_mmap(res_path):
-    view = ProguardMappingView.open(os.path.join(res_path, "proguard.txt"))
-    assert view.has_line_info
-    assert view.uuid == uuid.UUID("a48ca62b-df26-544e-a8b9-2a5ce210d1d5")
-
-    assert (
-        view.lookup("android.support.constraint.ConstraintLayout$a")
-        == "android.support.constraint.ConstraintLayout$LayoutParams"
-    )
-
-    assert (
-        view.lookup("android.support.constraint.a.b:a", 116)
-        == "android.support.constraint.solver.ArrayRow:createRowDefinition"
-    )
+from symbolic import ProguardMapper
 
 
 def test_mapper(res_path):
diff --git a/symbolic-debuginfo/tests/test_objects.rs b/symbolic-debuginfo/tests/test_objects.rs
index 8df844fc..1039b5cb 100644
--- a/symbolic-debuginfo/tests/test_objects.rs
+++ b/symbolic-debuginfo/tests/test_objects.rs
@@ -1,11 +1,11 @@
 use std::fmt;
 
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_debuginfo::{FileEntry, Function, Object, SymbolMap};
 use symbolic_testutils::fixture;
 
+type Error = Box<dyn std::error::Error>;
+
 /// Helper to create neat snapshots for symbol tables.
 struct SymbolsDebug<'a>(&'a SymbolMap<'a>);
 
diff --git a/symbolic-minidump/tests/test_cfi.rs b/symbolic-minidump/tests/test_cfi.rs
index ebc36739..92d3fc82 100644
--- a/symbolic-minidump/tests/test_cfi.rs
+++ b/symbolic-minidump/tests/test_cfi.rs
@@ -1,12 +1,12 @@
 use std::str;
 
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_debuginfo::Object;
 use symbolic_minidump::cfi::{AsciiCfiWriter, CfiCache};
 use symbolic_testutils::fixture;
 
+type Error = Box<dyn std::error::Error>;
+
 #[test]
 fn load_empty_cfi_cache() -> Result<(), Error> {
     let buffer = ByteView::from_slice(&[]);
diff --git a/symbolic-minidump/tests/test_processor.rs b/symbolic-minidump/tests/test_processor.rs
index 1a6a9b98..8d5e72d2 100644
--- a/symbolic-minidump/tests/test_processor.rs
+++ b/symbolic-minidump/tests/test_processor.rs
@@ -1,9 +1,9 @@
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_minidump::processor::ProcessState;
 use symbolic_testutils::fixture;
 
+type Error = Box<dyn std::error::Error>;
+
 #[test]
 fn process_minidump_linux() -> Result<(), Error> {
     let buffer = ByteView::open(fixture("linux/mini.dmp"))?;
diff --git a/symbolic-symcache/tests/test_cache.rs b/symbolic-symcache/tests/test_cache.rs
index 166b5ebc..6a3ef2f1 100644
--- a/symbolic-symcache/tests/test_cache.rs
+++ b/symbolic-symcache/tests/test_cache.rs
@@ -1,11 +1,11 @@
 use std::fmt;
 
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_symcache::SymCache;
 use symbolic_testutils::fixture;
 
+type Error = Box<dyn std::error::Error>;
+
 /// Helper to create neat snapshots for symbol tables.
 struct FunctionsDebug<'a>(&'a SymCache<'a>);
 
diff --git a/symbolic-symcache/tests/test_compat.rs b/symbolic-symcache/tests/test_compat.rs
index 0394dba7..423c0c14 100644
--- a/symbolic-symcache/tests/test_compat.rs
+++ b/symbolic-symcache/tests/test_compat.rs
@@ -1,11 +1,9 @@
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_symcache::SymCache;
 use symbolic_testutils::fixture;
 
 #[test]
-fn test_v1() -> Result<(), Error> {
+fn test_v1() -> Result<(), Box<dyn std::error::Error>> {
     let buffer = ByteView::open(fixture("symcache/compat/v1.symc"))?;
     let symcache = SymCache::parse(&buffer)?;
 
diff --git a/symbolic-symcache/tests/test_writer.rs b/symbolic-symcache/tests/test_writer.rs
index 9d860397..2e2df474 100644
--- a/symbolic-symcache/tests/test_writer.rs
+++ b/symbolic-symcache/tests/test_writer.rs
@@ -1,13 +1,13 @@
 use std::fmt;
 use std::io::Cursor;
 
-use failure::Error;
-
 use symbolic_common::ByteView;
 use symbolic_debuginfo::Object;
 use symbolic_symcache::{SymCache, SymCacheWriter};
 use symbolic_testutils::fixture;
 
+type Error = Box<dyn std::error::Error>;
+
 /// Helper to create neat snapshots for symbol tables.
 struct FunctionsDebug<'a>(&'a SymCache<'a>);