Add an option to Chain.capture() not to create an error zone (#36)

Some users (like test) manually convert traces to chains, so they
don't need the error zone behavior. In fact, it can cause problems,
such as those described in dart-lang/test#713.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0c972a..18ffda8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.9.0
+
+* Add an `errorZone` parameter to `Chain.capture()` that makes it avoid creating
+  an error zone.
+
 ## 1.8.3
 
 * `Chain.forTrace()` now returns a full stack chain for *all* `StackTrace`s
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 56c5333..8685a9e 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -65,13 +65,21 @@
   /// parent Zone's `unhandledErrorHandler` will be called with the error and
   /// its chain.
   ///
-  /// Note that even if [onError] isn't passed, this zone will still be an error
-  /// zone. This means that any errors that would cross the zone boundary are
-  /// considered unhandled.
+  /// If [errorZone] is `true`, the zone this creates will be an error zone,
+  /// even if [onError] isn't passed. This means that any errors that would
+  /// cross the zone boundary are considered unhandled. If [errorZone] is
+  /// `false`, [onError] must be `null`.
   ///
   /// If [callback] returns a value, it will be returned by [capture] as well.
   static T capture<T>(T callback(),
-      {void onError(error, Chain chain), bool when: true}) {
+      {void onError(error, Chain chain),
+      bool when: true,
+      bool errorZone: true}) {
+    if (!errorZone && onError != null) {
+      throw new ArgumentError.value(
+          onError, "onError", "must be null if errorZone is false");
+    }
+
     if (!when) {
       var newOnError;
       if (onError != null) {
@@ -87,7 +95,7 @@
       return runZoned(callback, onError: newOnError);
     }
 
-    var spec = new StackZoneSpecification(onError);
+    var spec = new StackZoneSpecification(onError, errorZone: errorZone);
     return runZoned(() {
       try {
         return callback();
diff --git a/lib/src/stack_zone_specification.dart b/lib/src/stack_zone_specification.dart
index cd5d068..5be8dd3 100644
--- a/lib/src/stack_zone_specification.dart
+++ b/lib/src/stack_zone_specification.dart
@@ -61,12 +61,16 @@
   /// The most recent node of the current stack chain.
   _Node _currentNode;
 
-  StackZoneSpecification([this._onError]);
+  /// Whether this is an error zone.
+  final bool _errorZone;
+
+  StackZoneSpecification(this._onError, {bool errorZone: true})
+      : _errorZone = errorZone;
 
   /// Converts [this] to a real [ZoneSpecification].
   ZoneSpecification toSpec() {
     return new ZoneSpecification(
-        handleUncaughtError: _handleUncaughtError,
+        handleUncaughtError: _errorZone ? _handleUncaughtError : null,
         registerCallback: _registerCallback,
         registerUnaryCallback: _registerUnaryCallback,
         registerBinaryCallback: _registerBinaryCallback,
diff --git a/pubspec.yaml b/pubspec.yaml
index 65d3ef6..3218be1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@
 #
 # When the major version is upgraded, you *must* update that version constraint
 # in pub to stay in sync with this.
-version: 1.8.3
+version: 1.9.0
 author: "Dart Team <misc@dartlang.org>"
 homepage: https://github.com/dart-lang/stack_trace
 description: A package for manipulating stack traces and printing them readably.
diff --git a/test/chain/chain_test.dart b/test/chain/chain_test.dart
index 407cc2b..26b04a1 100644
--- a/test/chain/chain_test.dart
+++ b/test/chain/chain_test.dart
@@ -38,33 +38,65 @@
     });
   });
 
-  group("Chain.capture() with when: false", () {
-    test("with no onError doesn't block errors", () {
-      expect(Chain.capture(() => new Future.error("oh no"), when: false),
-          throwsA("oh no"));
-    });
-
+  group("Chain.capture()", () {
     test("with onError blocks errors", () {
       Chain.capture(() {
         return new Future.error("oh no");
       }, onError: expectAsync2((error, chain) {
         expect(error, equals("oh no"));
         expect(chain, new isInstanceOf<Chain>());
-      }), when: false);
+      }));
     });
 
-    test("doesn't enable chain-tracking", () {
-      return Chain.disable(() {
-        return Chain.capture(() {
-          var completer = new Completer();
-          inMicrotask(() {
-            completer.complete(new Chain.current());
-          });
+    test("with no onError blocks errors", () {
+      runZoned(() {
+        var future =
+            Chain.capture(() => new Future.error("oh no"), when: false);
+        future.then(expectAsync1((_) {}, count: 0));
+      }, onError: expectAsync2((error, chain) {
+        expect(error, equals("oh no"));
+        expect(chain, new isInstanceOf<Chain>());
+      }));
+    });
 
-          return completer.future.then((chain) {
-            expect(chain.traces, hasLength(1));
-          });
-        }, when: false);
+    test("with errorZone: false doesn't block errors", () {
+      expect(Chain.capture(() => new Future.error("oh no"), errorZone: false),
+          throwsA("oh no"));
+    });
+
+    test("doesn't allow onError and errorZone: false", () {
+      expect(() => Chain.capture(() {}, onError: (_, __) {}, errorZone: false),
+          throwsArgumentError);
+    });
+
+    group("with when: false", () {
+      test("with no onError doesn't block errors", () {
+        expect(Chain.capture(() => new Future.error("oh no"), when: false),
+            throwsA("oh no"));
+      });
+
+      test("with onError blocks errors", () {
+        Chain.capture(() {
+          return new Future.error("oh no");
+        }, onError: expectAsync2((error, chain) {
+          expect(error, equals("oh no"));
+          expect(chain, new isInstanceOf<Chain>());
+        }), when: false);
+      });
+
+      test("doesn't enable chain-tracking", () {
+        return Chain.disable(() {
+          return Chain.capture(() {
+            var completer = new Completer();
+            inMicrotask(() {
+              completer.complete(new Chain.current());
+            });
+
+            return completer.future.then((chain) {
+              expect(chain.traces, hasLength(1));
+            });
+          }, when: false);
+        });
       });
     });
   });