diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..3a46861
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,181 @@
+use std::fmt;
+use std::fs;
+use std::io;
+
+use walkdir;
+
+use super::iter;
+
+/// The type of assertion that occurred.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum AssertionKind {
+    /// One of the two sides is missing.
+    Missing,
+    /// The two sides have different types.
+    FileType,
+    /// The content of the two sides is different.
+    Content,
+}
+
+impl AssertionKind {
+    /// Test if the assertion is from one of the two sides being missing.
+    pub fn is_missing(self) -> bool {
+        self == AssertionKind::Missing
+    }
+
+    /// Test if the assertion is from the two sides having different file types.
+    pub fn is_file_type(self) -> bool {
+        self == AssertionKind::FileType
+    }
+
+    /// Test if the assertion is from the two sides having different content.
+    pub fn is_content(self) -> bool {
+        self == AssertionKind::Content
+    }
+}
+
+/// Error to capture the difference between paths.
+#[derive(Debug, Clone)]
+pub struct AssertionError {
+    kind: AssertionKind,
+    entry: iter::DiffEntry,
+    msg: Option<String>,
+    cause: Option<IoError>,
+}
+
+impl AssertionError {
+    /// The type of difference detected.
+    pub fn kind(self) -> AssertionKind {
+        self.kind
+    }
+
+    /// Access to the `DiffEntry` for which a difference was detected.
+    pub fn entry(&self) -> &iter::DiffEntry {
+        &self.entry
+    }
+
+    /// Underlying error found when trying to find a difference
+    pub fn cause(&self) -> Option<&IoError> {
+        self.cause.as_ref()
+    }
+
+    /// Add an optional message to display with the error.
+    pub fn with_msg<S: Into<String>>(mut self, msg: S) -> Self {
+        self.msg = Some(msg.into());
+        self
+    }
+
+    /// Add an underlying error found when trying to find a difference.
+    pub fn with_cause<E: Into<IoError>>(mut self, err: E) -> Self {
+        self.cause = Some(err.into());
+        self
+    }
+
+    pub(crate) fn new(kind: AssertionKind, entry: iter::DiffEntry) -> Self {
+        Self {
+            kind,
+            entry,
+            msg: None,
+            cause: None,
+        }
+    }
+}
+
+impl fmt::Display for AssertionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.kind {
+            AssertionKind::Missing => {
+                write!(f,
+                       "One side is missing: {}\n  left: {:?}\n  right: {:?}",
+                       self.msg.as_ref().map(String::as_str).unwrap_or(""),
+                       self.entry.left().path(),
+                       self.entry.right().path())
+            }
+            AssertionKind::FileType => {
+                write!(f,
+                       "File types differ: {}\n  left: {:?} is {}\n  right: {:?} is {}",
+                       self.msg.as_ref().map(String::as_str).unwrap_or(""),
+                       self.entry.left().path(),
+                       display_file_type(self.entry.left().file_type()),
+                       self.entry.right().path(),
+                       display_file_type(self.entry.right().file_type()))
+            }
+            AssertionKind::Content => {
+                write!(f,
+                       "Content differs: {}\n  left: {:?}\n  right: {:?}",
+                       self.msg.as_ref().map(String::as_str).unwrap_or(""),
+                       self.entry.left().path(),
+                       self.entry.right().path())
+            }
+        }?;
+
+        if let Some(cause) = self.cause() {
+            write!(f, "\ncause: {}", cause)?;
+        }
+
+        Ok(())
+    }
+}
+
+fn display_file_type(file_type: Option<fs::FileType>) -> String {
+    if let Some(file_type) = file_type {
+        if file_type.is_file() {
+            "file".to_owned()
+        } else if file_type.is_dir() {
+            "dir".to_owned()
+        } else {
+            format!("{:?}", file_type)
+        }
+    } else {
+        "missing".to_owned()
+    }
+}
+
+/// IO errors preventing diffing from happening.
+#[derive(Debug, Clone)]
+pub struct IoError(InnerIoError);
+
+#[derive(Debug)]
+enum InnerIoError {
+    Io(io::Error),
+    WalkDir(walkdir::Error),
+    WalkDirEmpty,
+}
+
+impl Clone for InnerIoError {
+    fn clone(&self) -> Self {
+        match *self {
+            InnerIoError::Io(_) |
+            InnerIoError::WalkDirEmpty => self.clone(),
+            InnerIoError::WalkDir(_) => InnerIoError::WalkDirEmpty,
+        }
+    }
+}
+
+impl fmt::Display for IoError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl fmt::Display for InnerIoError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            InnerIoError::Io(ref e) => e.fmt(f),
+            InnerIoError::WalkDir(ref e) => e.fmt(f),
+            InnerIoError::WalkDirEmpty => write!(f, "Unknown error when walking"),
+        }
+    }
+}
+
+impl From<io::Error> for IoError {
+    fn from(e: io::Error) -> IoError {
+        IoError(InnerIoError::Io(e))
+    }
+}
+
+impl From<walkdir::Error> for IoError {
+    fn from(e: walkdir::Error) -> IoError {
+        IoError(InnerIoError::WalkDir(e))
+    }
+}
diff --git a/src/iter.rs b/src/iter.rs
new file mode 100644
index 0000000..55d2f54
--- /dev/null
+++ b/src/iter.rs
@@ -0,0 +1,321 @@
+use std::io::prelude::*;
+use std::ffi;
+use std::fs;
+use std::io;
+use std::path;
+
+use walkdir;
+
+use error::IoError;
+use error::{AssertionKind, AssertionError};
+
+type WalkIter = walkdir::IntoIter;
+
+/// A builder to create an iterator for recusively diffing two directories.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DirDiff {
+    left: path::PathBuf,
+    right: path::PathBuf,
+}
+
+impl DirDiff {
+    /// Create a builder for recursively diffing two directories, starting at `left_root` and
+    /// `right_root`.
+    pub fn new<L, R>(left_root: L, right_root: R) -> Self
+        where L: Into<path::PathBuf>,
+              R: Into<path::PathBuf>
+    {
+        Self {
+            left: left_root.into(),
+            right: right_root.into(),
+        }
+    }
+
+    fn walk(path: &path::Path) -> WalkIter {
+        walkdir::WalkDir::new(path).min_depth(1).into_iter()
+    }
+}
+
+impl IntoIterator for DirDiff {
+    type Item = Result<DiffEntry, IoError>;
+
+    type IntoIter = IntoIter;
+
+    fn into_iter(self) -> IntoIter {
+        let left_walk = Self::walk(&self.left);
+        let right_walk = Self::walk(&self.right);
+        IntoIter {
+            left_root: self.left,
+            left_walk,
+            right_root: self.right,
+            right_walk,
+        }
+    }
+}
+
+/// A potential directory entry.
+///
+/// # Differences with `std::fs::DirEntry`
+///
+/// This mostly mirrors `DirEntry` in `std::fs` and `walkdir`
+///
+/// * The path might not actually exist.  In this case, `.file_type()` returns `None`.
+/// * Borroed information is returned
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct DirEntry {
+    path: path::PathBuf,
+    file_type: Option<fs::FileType>,
+}
+
+impl DirEntry {
+    /// The full path that this entry represents.
+    pub fn path(&self) -> &path::Path {
+        self.path.as_path()
+    }
+
+    /// Returns the metadata for the file that this entry points to.
+    pub fn metadata(&self) -> Result<fs::Metadata, IoError> {
+        let m = fs::metadata(&self.path)?;
+        Ok(m)
+    }
+
+    /// Returns the file type for the file that this entry points to.
+    ///
+    /// The `Option` is `None` if the file does not exist.
+    pub fn file_type(&self) -> Option<fs::FileType> {
+        self.file_type
+    }
+
+    /// Returns the file name of this entry.
+    ///
+    /// If this entry has no file name (e.g. `/`), then the full path is returned.
+    pub fn file_name(&self) -> &ffi::OsStr {
+        self.path
+            .file_name()
+            .unwrap_or_else(|| self.path.as_os_str())
+    }
+
+    pub(self) fn exists(path: path::PathBuf) -> Result<Self, IoError> {
+        let metadata = fs::symlink_metadata(&path)?;
+        let file_type = Some(metadata.file_type());
+        let s = Self { path, file_type };
+        Ok(s)
+    }
+
+    pub(self) fn missing(path: path::PathBuf) -> Result<Self, IoError> {
+        let file_type = None;
+        let s = Self { path, file_type };
+        Ok(s)
+    }
+}
+
+/// To paths to compare.
+///
+/// This is the type of value that is yielded from `IntoIter`.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct DiffEntry {
+    left: DirEntry,
+    right: DirEntry,
+}
+
+impl DiffEntry {
+    /// The entry for the left tree.
+    ///
+    /// This will always be returned, even if the entry does not exist.  See `DirEntry::file_type`
+    /// to see how to check if the path exists.
+    pub fn left(&self) -> &DirEntry {
+        &self.left
+    }
+
+    /// The entry for the right tree.
+    ///
+    /// This will always be returned, even if the entry does not exist.  See `DirEntry::file_type`
+    /// to see how to check if the path exists.
+    pub fn right(&self) -> &DirEntry {
+        &self.right
+    }
+
+    /// Embed the `DiffEntry` into an `AssertionError` for convinience when writing assertions.
+    pub fn into_error(self, kind: AssertionKind) -> AssertionError {
+        AssertionError::new(kind, self)
+    }
+
+    /// Returns an error if the two paths are different.
+    ///
+    /// If this default policy does not work for you, you can use the constinuent assertions
+    /// (e.g. `assert_exists).
+    pub fn assert(self) -> Result<Self, AssertionError> {
+        match self.file_types() {
+            (Some(left), Some(right)) => {
+                if left != right {
+                    Err(self.into_error(AssertionKind::FileType))
+                } else if left.is_file() {
+                    // Because of the `left != right` test, we can assume `right` is also a file.
+                    match self.content_matches() {
+                        Ok(true) => Ok(self),
+                        Ok(false) => Err(self.into_error(AssertionKind::Content)),
+                        Err(e) => Err(self.into_error(AssertionKind::Content).with_cause(e)),
+                    }
+                } else {
+                    Ok(self)
+                }
+            }
+            _ => Err(self.into_error(AssertionKind::Missing)),
+        }
+    }
+
+    /// Returns an error iff one of the two paths does not exist.
+    pub fn assert_exists(self) -> Result<Self, AssertionError> {
+        match self.file_types() {
+            (Some(_), Some(_)) => Ok(self),
+            _ => Err(self.into_error(AssertionKind::Missing)),
+        }
+    }
+
+    /// Returns an error iff two paths are of different types.
+    pub fn assert_file_type(self) -> Result<Self, AssertionError> {
+        match self.file_types() {
+            (Some(left), Some(right)) => {
+                if left != right {
+                    Err(self.into_error(AssertionKind::FileType))
+                } else {
+                    Ok(self)
+                }
+            }
+            _ => Ok(self),
+        }
+    }
+
+    /// Returns an error iff the file content of the two paths is different.
+    ///
+    /// This is assuming they are both files.
+    pub fn assert_content(self) -> Result<Self, AssertionError> {
+        if !self.are_files() {
+            return Ok(self);
+        }
+
+        match self.content_matches() {
+            Ok(true) => Ok(self),
+            Ok(false) => Err(self.into_error(AssertionKind::Content)),
+            Err(e) => Err(self.into_error(AssertionKind::Content).with_cause(e)),
+        }
+    }
+
+    fn file_types(&self) -> (Option<fs::FileType>, Option<fs::FileType>) {
+        let left = self.left.file_type();
+        let right = self.right.file_type();
+        (left, right)
+    }
+
+    fn are_files(&self) -> bool {
+        let (left, right) = self.file_types();
+        let left = left.as_ref().map(fs::FileType::is_file).unwrap_or(false);
+        let right = right.as_ref().map(fs::FileType::is_file).unwrap_or(false);
+        left && right
+    }
+
+    fn content_matches(&self) -> Result<bool, IoError> {
+        const CAP: usize = 1024;
+
+        let left_file = fs::File::open(self.left.path())?;
+        let mut left_buf = io::BufReader::with_capacity(CAP, left_file);
+
+        let right_file = fs::File::open(self.right.path())?;
+        let mut right_buf = io::BufReader::with_capacity(CAP, right_file);
+
+        loop {
+            let length = {
+                let left = left_buf.fill_buf()?;
+                let right = right_buf.fill_buf()?;
+                if left != right {
+                    return Ok(false);
+                }
+
+                assert_eq!(left.len(),
+                           right.len(),
+                           "Above check should ensure lengths are the same");
+                left.len()
+            };
+            if length == 0 {
+                break;
+            }
+            left_buf.consume(length);
+            right_buf.consume(length);
+        }
+
+        Ok(true)
+    }
+}
+
+/// An iterator for recursively diffing two directories.
+///
+/// To create an `IntoIter`, first create the builder `DirDiff` and call `.into_iter()`.
+#[derive(Debug)]
+pub struct IntoIter {
+    pub(self) left_root: path::PathBuf,
+    pub(self) left_walk: WalkIter,
+    pub(self) right_root: path::PathBuf,
+    pub(self) right_walk: WalkIter,
+}
+
+impl IntoIter {
+    fn transposed_next(&mut self) -> Result<Option<DiffEntry>, IoError> {
+        if let Some(entry) = self.left_walk.next() {
+            let entry = entry?;
+            let entry_path = entry.path();
+
+            let relative = entry_path
+                .strip_prefix(&self.left_root)
+                .expect("WalkDir returns items rooted under left_root");
+            let right = self.right_root.join(relative);
+            let right = if right.exists() {
+                DirEntry::exists(right)
+            } else {
+                DirEntry::missing(right)
+            }?;
+
+            // Don't use `walkdir::DirEntry` because its `file_type` came from `fs::read_dir`
+            // which we can't reproduce for `right`
+            let left = DirEntry::exists(entry_path.to_owned())?;
+
+            let entry = DiffEntry { left, right };
+            return Ok(Some(entry));
+        }
+
+        while let Some(entry) = self.right_walk.next() {
+            let entry = entry?;
+            let entry_path = entry.path();
+
+            let relative = entry_path
+                .strip_prefix(&self.right_root)
+                .expect("WalkDir returns items rooted under right_root");
+            let left = self.left_root.join(relative);
+            // `left.exists()` was covered above
+            if !left.exists() {
+                let left = DirEntry::missing(left)?;
+
+                // Don't use `walkdir::DirEntry` because its `file_type` came from `fs::read_dir`
+                // which we can't reproduce for `left`
+                let right = DirEntry::exists(entry_path.to_owned())?;
+
+                let entry = DiffEntry { left, right };
+                return Ok(Some(entry));
+            }
+        }
+
+        Ok(None)
+    }
+}
+
+impl Iterator for IntoIter {
+    type Item = Result<DiffEntry, IoError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let item = self.transposed_next();
+        match item {
+            Ok(Some(i)) => Some(Ok(i)),
+            Ok(None) => None,
+            Err(e) => Some(Err(e)),
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 1cce8bd..268c95b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,20 +13,14 @@
 
 extern crate walkdir;
 
-use std::fs::File;
-use std::io::prelude::*;
-use std::path::Path;
-use std::cmp::Ordering;
+mod error;
+mod iter;
 
-use walkdir::{DirEntry, WalkDir};
+use std::path::PathBuf;
 
-/// The various errors that can happen when diffing two directories
-#[derive(Debug)]
-pub enum Error {
-    Io(std::io::Error),
-    StripPrefix(std::path::StripPrefixError),
-    WalkDir(walkdir::Error),
-}
+pub use error::IoError;
+pub use error::{AssertionKind, AssertionError};
+pub use iter::{DirDiff, DirEntry, DiffEntry, IntoIter};
 
 /// Are the contents of two directories different?
 ///
@@ -37,59 +31,15 @@ pub enum Error {
 ///
 /// assert!(dir_diff::is_different("dir/a", "dir/b").unwrap());
 /// ```
-pub fn is_different<A: AsRef<Path>, B: AsRef<Path>>(a_base: A, b_base: B) -> Result<bool, Error> {
-    let mut a_walker = walk_dir(a_base);
-    let mut b_walker = walk_dir(b_base);
-
-    for (a, b) in (&mut a_walker).zip(&mut b_walker) {
-        let a = a?;
-        let b = b?;
-
-        if a.depth() != b.depth() || a.file_type() != b.file_type()
-            || a.file_name() != b.file_name()
-            || (a.file_type().is_file() && read_to_vec(a.path())? != read_to_vec(b.path())?)
-        {
+pub fn is_different<L, R>(left_root: L, right_root: R) -> Result<bool, IoError>
+    where L: Into<PathBuf>,
+          R: Into<PathBuf>
+{
+    for entry in iter::DirDiff::new(left_root, right_root) {
+        if entry?.assert().is_err() {
             return Ok(true);
         }
     }
 
-    Ok(!a_walker.next().is_none() || !b_walker.next().is_none())
-}
-
-fn walk_dir<P: AsRef<Path>>(path: P) -> std::iter::Skip<walkdir::IntoIter> {
-    WalkDir::new(path)
-        .sort_by(compare_by_file_name)
-        .into_iter()
-        .skip(1)
-}
-
-fn compare_by_file_name(a: &DirEntry, b: &DirEntry) -> Ordering {
-    a.file_name().cmp(b.file_name())
-}
-
-fn read_to_vec<P: AsRef<Path>>(file: P) -> Result<Vec<u8>, std::io::Error> {
-    let mut data = Vec::new();
-    let mut file = File::open(file.as_ref())?;
-
-    file.read_to_end(&mut data)?;
-
-    Ok(data)
-}
-
-impl From<std::io::Error> for Error {
-    fn from(e: std::io::Error) -> Error {
-        Error::Io(e)
-    }
-}
-
-impl From<std::path::StripPrefixError> for Error {
-    fn from(e: std::path::StripPrefixError) -> Error {
-        Error::StripPrefix(e)
-    }
-}
-
-impl From<walkdir::Error> for Error {
-    fn from(e: walkdir::Error) -> Error {
-        Error::WalkDir(e)
-    }
+    Ok(false)
 }