Handle CustomMatcher errors better (#50)

Fixes #25 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9527173..f36f40d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.12.1+1
+
+* Produce a better error message when a `CustomMatcher`'s feature throws.
+
 ## 0.12.1
 
 * Add containsAllInOrder matcher for Iterables
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
index c75739d..77782df 100644
--- a/lib/src/core_matchers.dart
+++ b/lib/src/core_matchers.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:stack_trace/stack_trace.dart';
+
 import 'description.dart';
 import 'interfaces.dart';
 import 'util.dart';
@@ -261,7 +263,7 @@
 
   Description describeMismatch(
       item, Description mismatchDescription, Map matchState, bool verbose) {
-    var reason = matchState['reason'];
+    var reason = matchState['reason'] ?? '';
     // If we didn't get a good reason, that would normally be a
     // simple 'is <value>' message. We only add that if the mismatch
     // description is non empty (so we are supplementing the mismatch
@@ -614,9 +616,23 @@
   featureValueOf(actual) => actual;
 
   bool matches(item, Map matchState) {
-    var f = featureValueOf(item);
-    if (_matcher.matches(f, matchState)) return true;
-    addStateInfo(matchState, {'feature': f});
+    try {
+      var f = featureValueOf(item);
+      if (_matcher.matches(f, matchState)) return true;
+      addStateInfo(matchState, {'custom.feature': f});
+    } catch (exception, stack) {
+      addStateInfo(matchState, {
+        'custom.exception': exception.toString(),
+        'custom.stack': new Chain.forTrace(stack)
+            .foldFrames(
+                (frame) =>
+                    frame.package == 'test' ||
+                    frame.package == 'stream_channel' ||
+                    frame.package == 'matcher',
+                terse: true)
+            .toString()
+      });
+    }
     return false;
   }
 
@@ -625,14 +641,25 @@
 
   Description describeMismatch(
       item, Description mismatchDescription, Map matchState, bool verbose) {
+    if (matchState['custom.exception'] != null) {
+      mismatchDescription
+          .add('threw ')
+          .addDescriptionOf(matchState['custom.exception'])
+          .add('\n')
+          .add(matchState['custom.stack'].toString());
+      return mismatchDescription;
+    }
+
     mismatchDescription
         .add('has ')
         .add(_featureName)
         .add(' with value ')
-        .addDescriptionOf(matchState['feature']);
+        .addDescriptionOf(matchState['custom.feature']);
     var innerDescription = new StringDescription();
-    _matcher.describeMismatch(
-        matchState['feature'], innerDescription, matchState['state'], verbose);
+
+    _matcher.describeMismatch(matchState['custom.feature'], innerDescription,
+        matchState['state'], verbose);
+
     if (innerDescription.length > 0) {
       mismatchDescription.add(' which ').add(innerDescription.toString());
     }
diff --git a/pubspec.yaml b/pubspec.yaml
index 4bb9887..2a5ba6b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: matcher
 
-version: 0.12.1
+version: 0.12.1+1
 author: Dart Team <misc@dartlang.org>
 description: Support for specifying test expectations
 homepage: https://github.com/dart-lang/matcher
diff --git a/test/core_matchers_test.dart b/test/core_matchers_test.dart
index a31391b..04cc111 100644
--- a/test/core_matchers_test.dart
+++ b/test/core_matchers_test.dart
@@ -7,6 +7,11 @@
 
 import 'test_utils.dart';
 
+class BadCustomMatcher extends CustomMatcher {
+  BadCustomMatcher() : super("feature", "description", {1: "a"});
+  featureValueOf(actual) => throw new Exception("bang");
+}
+
 void main() {
   test('isTrue', () {
     shouldPass(true, isTrue);
@@ -236,4 +241,17 @@
         "Which: has price with value <10> which is not "
         "a value greater than <10>");
   });
+
+  test("Custom Matcher Exception", () {
+    shouldFail(
+        "a",
+        new BadCustomMatcher(),
+        allOf([
+          contains("Expected: feature {1: 'a'} "),
+          contains("Actual: 'a' "),
+          contains("Which: threw 'Exception: bang' "),
+          contains("test/core_matchers_test.dart "),
+          contains("package:test ")
+        ]));
+  });
 }