diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bfa5202cfa64cd41dbf78725e3919603b521290..1cd35ffd5d5ddcc162c150e1d144e5473051a84d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
 
 * Fix running VM tests against `pub serve`.
 
+* More gracefully handle browser errors.
+
 * Properly load Dartium from the Dart Editor when possible.
 
 ### 0.12.0-beta.8
diff --git a/lib/src/runner/browser/chrome.dart b/lib/src/runner/browser/chrome.dart
index fc4e232920e60dbc875b5f29550c94aab2d4f0c5..bca71a48994976cf5dc3e268b9a8d2e95a08f318 100644
--- a/lib/src/runner/browser/chrome.dart
+++ b/lib/src/runner/browser/chrome.dart
@@ -5,11 +5,15 @@
 library test.runner.browser.chrome;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
 
 import '../../util/io.dart';
+import '../../utils.dart';
+import '../application_exception.dart';
 import 'browser.dart';
 
 // TODO(nweiz): move this into its own package?
@@ -25,12 +29,6 @@ class Chrome implements Browser {
   /// The underlying process.
   Process _process;
 
-  /// The temporary directory used as the browser's user data dir.
-  ///
-  /// A new data dir is created for each run to ensure that they're
-  /// well-isolated.
-  String _dir;
-
   Future get onExit => _onExitCompleter.future;
   final _onExitCompleter = new Completer();
 
@@ -52,9 +50,8 @@ class Chrome implements Browser {
     // for the process to actually start. They should just wait for the HTTP
     // request instead.
     withTempDir((dir) {
-      _dir = dir;
       return Process.start(executable, [
-        "--user-data-dir=$_dir",
+        "--user-data-dir=$dir",
         url.toString(),
         "--disable-extensions",
         "--disable-popup-blocking",
@@ -72,9 +69,19 @@ class Chrome implements Browser {
         return _process.exitCode;
       });
     }).then((exitCode) {
-      if (exitCode != 0) throw "Chrome failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+      if (exitCode == 0) return null;
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "Chrome failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start Chrome: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/content_shell.dart b/lib/src/runner/browser/content_shell.dart
index 2df48c32f4556fc00e457b6eb2158be793f3ea7e..6849e391a8ea83923021718c8d039e76703b04d6 100644
--- a/lib/src/runner/browser/content_shell.dart
+++ b/lib/src/runner/browser/content_shell.dart
@@ -8,6 +8,9 @@ import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:stack_trace/stack_trace.dart';
+
+import '../../utils.dart';
 import '../application_exception.dart';
 import 'browser.dart';
 
@@ -71,9 +74,20 @@ class ContentShell implements Browser {
                 "latest/dartium/");
       }
 
-      if (exitCode != 0) throw "Content shell failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+      if (exitCode == 0) return null;
+
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "Content shell failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start content shell: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/dartium.dart b/lib/src/runner/browser/dartium.dart
index f8d81513e00bad2cd386b5ad018676fbc8d323b8..627b17d32db2c3ca0188210b2744a356a32a049a 100644
--- a/lib/src/runner/browser/dartium.dart
+++ b/lib/src/runner/browser/dartium.dart
@@ -5,11 +5,15 @@
 library test.runner.browser.dartium;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
 
 import '../../util/io.dart';
+import '../../utils.dart';
+import '../application_exception.dart';
 import 'browser.dart';
 
 /// A class for running an instance of Dartium.
@@ -23,12 +27,6 @@ class Dartium implements Browser {
   /// The underlying process.
   Process _process;
 
-  /// The temporary directory used as the browser's user data dir.
-  ///
-  /// A new data dir is created for each run to ensure that they're
-  /// well-isolated.
-  String _dir;
-
   Future get onExit => _onExitCompleter.future;
   final _onExitCompleter = new Completer();
 
@@ -50,9 +48,8 @@ class Dartium implements Browser {
     // for the process to actually start. They should just wait for the HTTP
     // request instead.
     withTempDir((dir) {
-      _dir = dir;
       return Process.start(executable, [
-        "--user-data-dir=$_dir",
+        "--user-data-dir=$dir",
         url.toString(),
         "--disable-extensions",
         "--disable-popup-blocking",
@@ -70,9 +67,19 @@ class Dartium implements Browser {
         return _process.exitCode;
       });
     }).then((exitCode) {
-      if (exitCode != 0) throw "Dartium failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+      if (exitCode == 0) return null;
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "Dartium failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start Dartium: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/firefox.dart b/lib/src/runner/browser/firefox.dart
index 215f3a85885d77cbe0cf621dc670c6b051d21c98..36eed533adbe14817651f3381a90cfbe6ddbb00c 100644
--- a/lib/src/runner/browser/firefox.dart
+++ b/lib/src/runner/browser/firefox.dart
@@ -5,11 +5,15 @@
 library test.runner.browser.firefox;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
 
 import '../../util/io.dart';
+import '../../utils.dart';
+import '../application_exception.dart';
 import 'browser.dart';
 
 final _preferences = '''
@@ -29,12 +33,6 @@ class Firefox implements Browser {
   /// The underlying process.
   Process _process;
 
-  /// The temporary directory used as the browser's user data dir.
-  ///
-  /// A new data dir is created for each run to ensure that they're
-  /// well-isolated.
-  String _dir;
-
   Future get onExit => _onExitCompleter.future;
   final _onExitCompleter = new Completer();
 
@@ -56,13 +54,11 @@ class Firefox implements Browser {
     // for the process to actually start. They should just wait for the HTTP
     // request instead.
     withTempDir((dir) {
-      _dir = dir;
-
       new File(p.join(dir, 'prefs.js')).writeAsStringSync(_preferences);
 
       return Process.start(executable, [
         "--profile",
-        "$_dir",
+        "$dir",
         url.toString(),
         "--no-remote"
       ], environment: {
@@ -76,9 +72,19 @@ class Firefox implements Browser {
         return _process.exitCode;
       });
     }).then((exitCode) {
-      if (exitCode != 0) throw "Firefox failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+      if (exitCode == 0) return null;
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "Firefox failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start Firefox: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/phantom_js.dart b/lib/src/runner/browser/phantom_js.dart
index 0eadafb4512d767c8558ae00c2a476508469f4da..c13ee63fb6c24f53f5056d635ddb3e98f504d660 100644
--- a/lib/src/runner/browser/phantom_js.dart
+++ b/lib/src/runner/browser/phantom_js.dart
@@ -5,12 +5,15 @@
 library test.runner.browser.phantom_js;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
 
 import '../../util/exit_codes.dart' as exit_codes;
 import '../../util/io.dart';
+import '../../utils.dart';
 import '../application_exception.dart';
 import 'browser.dart';
 
@@ -82,9 +85,20 @@ class PhantomJS implements Browser {
         throw new ApplicationException(
             "Only PhantomJS version 2.0.0 or greater is supported.");
       }
-      if (exitCode != 0) throw "PhantomJS failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+
+      if (exitCode == 0) return null;
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "PhantomJS failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start PhantomJS: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/safari.dart b/lib/src/runner/browser/safari.dart
index 4ad28da4a41180fb1df9a2cbf837930e18fa4728..af697850b282d954d87c23a39554a494d9a3d664 100644
--- a/lib/src/runner/browser/safari.dart
+++ b/lib/src/runner/browser/safari.dart
@@ -9,8 +9,11 @@ import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
 
 import '../../util/io.dart';
+import '../../utils.dart';
+import '../application_exception.dart';
 import 'browser.dart';
 
 /// A class for running an instance of Safari.
@@ -59,9 +62,19 @@ class Safari implements Browser {
         return _process.exitCode;
       });
     }).then((exitCode) {
-      if (exitCode != 0) throw "Safari failed with exit code $exitCode.";
-    }).then(_onExitCompleter.complete)
-        .catchError(_onExitCompleter.completeError);
+      if (exitCode == 0) return null;
+
+      return UTF8.decodeStream(_process.stderr).then((error) {
+        throw new ApplicationException(
+            "Safari failed with exit code $exitCode:\n$error");
+      });
+    }).then(_onExitCompleter.complete).catchError((error, stackTrace) {
+      if (stackTrace == null) stackTrace = new Trace.current();
+      _onExitCompleter.completeError(
+          new ApplicationException(
+              "Failed to start Safari: ${getErrorMessage(error)}."),
+          stackTrace);
+    });
   }
 
   Future close() {
diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart
index fb0e55abddf6f16b6d0569b6715785ac54854bd8..17862ebcb7fa4b844b4d49782f8f2a60442c7361 100644
--- a/lib/src/runner/browser/server.dart
+++ b/lib/src/runner/browser/server.dart
@@ -266,7 +266,7 @@ void main() {
 
       // TODO(nweiz): Don't start the browser until all the suites are compiled.
       return _browserManagerFor(browser).then((browserManager) {
-        if (_closed) return null;
+        if (_closed || browserManager == null) return null;
         return browserManager.loadSuite(path, suiteUrl, metadata);
       }).then((suite) {
         if (_closed) return null;