diff --git a/lib/src/io.dart b/lib/src/io.dart
index c0b5c2b875df897ecfe989ef7444439b48ec54dd..52752b15209bb4c9875f646d6de35b0f33554695 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -270,49 +270,72 @@ List<String> listDir(String dir, {bool recursive: false,
 /// a symlink only if that symlink is unbroken and points to a directory.
 bool dirExists(String dir) => new Directory(dir).existsSync();
 
-/// Deletes whatever's at [path], whether it's a file, directory, or symlink. If
-/// it's a directory, it will be deleted recursively.
-void deleteEntry(String path) {
-  tryDeleteEntry() {
-    if (linkExists(path)) {
-      log.io("Deleting link $path.");
-      new Link(path).deleteSync();
-    } else if (dirExists(path)) {
-      log.io("Deleting directory $path.");
-      new Directory(path).deleteSync(recursive: true);
-    } else if (fileExists(path)) {
-      log.io("Deleting file $path.");
-      new File(path).deleteSync();
-    }
-  }
-
+/// Try to resiliently perform [operation].
+///
+/// Some file system operations can intermittently fail on Windows because
+/// other processes are locking a file. We've seen this with virus scanners
+/// when we try to delete or move something while it's being scanned. To
+/// mitigate that, on Windows, this will retry the operation a few times if it
+/// fails.
+void _attempt(String description, void operation()) {
   if (Platform.operatingSystem != 'windows') {
-    tryDeleteEntry();
+    operation();
     return;
   }
 
-  // On Windows, we can fail to delete an entry if it's in use by another
-  // process. The only case where we know this to cause a problem is when
-  // testing "pub serve", since it can poll a file at the same time we try to
-  // delete it in the test process (issue 16129).
-  //
-  // TODO(nweiz): Once issue 14428 is fixed for Windows, remove this special
-  // handling.
+  getErrorReason(error) {
+    if (error.osError.errorCode == 5) {
+      return "access was denied";
+    }
+
+    if (error.osError.errorCode == 32) {
+      return "it was in use by another process";
+    }
+
+    return null;
+  }
+
   for (var i = 0; i < 2; i++) {
     try {
-      tryDeleteEntry();
+      operation();
+      return;
     } on FileSystemException catch (error) {
-      // Errno 32 indicates that the deletion failed because the file was in
-      // use.
-      if (error.osError.errorCode != 32) rethrow;
+      var reason = getErrorReason(error);
+      if (reason == null) rethrow;
 
-      log.io("Failed to delete entry because it was in use by another process. "
+      log.io("Failed to $description because $reason. "
           "Retrying in 50ms.");
       sleep(new Duration(milliseconds: 50));
     }
   }
 
-  tryDeleteEntry();
+  try {
+    operation();
+  } on FileSystemException catch (error) {
+    var reason = getErrorReason(error);
+    if (reason == null) rethrow;
+
+    fail("Failed to $description because $reason.\n"
+        "This may be caused by a virus scanner or having a file\n"
+        "in the directory open in another application.");
+  }
+}
+
+/// Deletes whatever's at [path], whether it's a file, directory, or symlink. If
+/// it's a directory, it will be deleted recursively.
+void deleteEntry(String path) {
+  _attempt("delete entry", () {
+    if (linkExists(path)) {
+      log.io("Deleting link $path.");
+      new Link(path).deleteSync();
+    } else if (dirExists(path)) {
+      log.io("Deleting directory $path.");
+      new Directory(path).deleteSync(recursive: true);
+    } else if (fileExists(path)) {
+      log.io("Deleting file $path.");
+      new File(path).deleteSync();
+    }
+  });
 }
 
 /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a
@@ -324,14 +347,16 @@ void cleanDir(String dir) {
 
 /// Renames (i.e. moves) the directory [from] to [to].
 void renameDir(String from, String to) {
-  log.io("Renaming directory $from to $to.");
-  try {
-    new Directory(from).renameSync(to);
-  } on IOException catch (error) {
-    // Ensure that [to] isn't left in an inconsistent state. See issue 12436.
-    if (entryExists(to)) deleteEntry(to);
-    rethrow;
-  }
+  _attempt("rename directory", () {
+    log.io("Renaming directory $from to $to.");
+    try {
+      new Directory(from).renameSync(to);
+    } on IOException catch (error) {
+      // Ensure that [to] isn't left in an inconsistent state. See issue 12436.
+      if (entryExists(to)) deleteEntry(to);
+      rethrow;
+    }
+  });
 }
 
 /// Creates a new symlink at path [symlink] that points to [target]. Returns a