Repository: mochajs/mocha

Problem statement:
Watcher crashes when reloading code that throws (regression mocha@7 to mocha@8)
<!--
Have you read Mocha's Code of Conduct? By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/mochajs/mocha/blob/master/.github/CODE_OF_CONDUCT.md
For more, check out the Mocha Gitter chat room: https://gitter.im/mochajs/mocha

Detail the steps necessary to reproduce the problem. To get the fastest support, create an MCVE and upload it to GitHub.
create an [MCVE](https://stackoverflow.com/help/mcve) and upload it to GitHub.
-->

### Prerequisites

<!--
Place an `x` between the square brackets on the lines below for every satisfied prerequisite.
-->

- [x] Checked that your issue hasn't already been filed by cross-referencing [issues with the `faq` label](https://github.com/mochajs/mocha/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq%20)
- [x] Checked next-gen ES issues and syntax problems by using the same environment and/or transpiler configuration without Mocha to ensure it isn't just a feature that actually isn't supported in the environment in question or a bug in your code.
- [x] 'Smoke tested' the code to be tested by running it outside the real test suite to get a better sense of whether the problem is in the code under test, your usage of Mocha, or Mocha itself
- [x] Ensured that there is no discrepancy between the locally and globally installed versions of Mocha. You can find them with: `node node_modules/.bin/mocha --version`(Local) and `mocha --version`(Global). We recommend that you _not_ install Mocha globally.

### Description

MCVE: please see https://github.com/marcelbeumer/mocha-8-watcher-issue for a minimal test case with STR.

Since mocha@8 the watcher crashes when reloading tests and a require fails, for example when causing a syntax error. Errors that happen during test execution are handled correctly by the watcher. Experimenting with different mocha versions, it turns out it is a regression from v7 to v8.

### Steps to Reproduce

See MCVE: https://github.com/marcelbeumer/mocha-8-watcher-issue. It seems that when the watcher is reloading code that will throw when requiring, the watcher crashes.

**Expected behavior:**

The watcher does not crash when reloading test code. Behavior for the initial test run when starting the watcher is debatable but would be best if it handled both cases well.

**Actual behavior:** 

The watcher crashes when reloading test code that causes an error.

**Reproduces how often:** 

100%

### Versions

<!-- If applicable, please specify: -->

- The output of `mocha --version` and `node node_modules/.bin/mocha --version`: 8.30 (but also tried @8.0, and @7)
- The output of `node --version`: v15.6.0
- Your operating system
  - name and version: macOS 11.1
  - architecture (32 or 64-bit): 64
- Your shell (e.g., bash, zsh, PowerShell, cmd): zsh
- Your browser and version (if running browser tests):
- Any third-party Mocha-related modules (and their versions):
- Any code transpiler (e.g., TypeScript, CoffeeScript, Babel) being used (and its version): no, but same behavior with ts-node setup (see branch in https://github.com/marcelbeumer/mocha-8-watcher-issue)

### Additional Information

<!--
Any additional information, configuration or data that might be necessary to reproduce the issue.
-->




Hints:
When I tested your MCVE(nice MCVE), it only happened with node.js 15.x.
With 14.x and mocha v8 watcher is fine. 

It seems it caused by node 15.x. But I'm not sure because mocha v7 is fine with node 15.x.
Didn't Node 15 change handling of unhandled rejections to throw?
Maybe this in combination with mocha 8 refactorings for running things in parallel?
@marcelbeumer Yes, Node 15's [default mode for unhandledRejection is changed to throw (from warn)](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V15.md#throw-on-unhandled-rejections---33021).
I am also strongly suspicious of this issue, but I have not yet confirmed it.

Patch:
diff --git a/lib/cli/watch-run.js b/lib/cli/watch-run.js
--- a/lib/cli/watch-run.js
+++ b/lib/cli/watch-run.js
@@ -261,17 +261,21 @@ const createRerunner = (mocha, watcher, {beforeRun} = {}) => {
   let rerunScheduled = false;
 
   const run = () => {
-    mocha = beforeRun ? beforeRun({mocha, watcher}) || mocha : mocha;
-    runner = mocha.run(() => {
-      debug('finished watch run');
-      runner = null;
-      blastCache(watcher);
-      if (rerunScheduled) {
-        rerun();
-      } else {
-        console.error(`${logSymbols.info} [mocha] waiting for changes...`);
-      }
-    });
+    try {
+      mocha = beforeRun ? beforeRun({mocha, watcher}) || mocha : mocha;
+      runner = mocha.run(() => {
+        debug('finished watch run');
+        runner = null;
+        blastCache(watcher);
+        if (rerunScheduled) {
+          rerun();
+        } else {
+          console.error(`${logSymbols.info} [mocha] waiting for changes...`);
+        }
+      });
+    } catch (e) {
+      console.error(e.stack);
+    }
   };
 
   const scheduleRun = () => {


Test patch:
diff --git a/test/integration/helpers.js b/test/integration/helpers.js
--- a/test/integration/helpers.js
+++ b/test/integration/helpers.js
@@ -470,6 +470,7 @@ async function runMochaWatchJSONAsync(args, opts, change) {
       // eslint-disable-next-line no-control-regex
       .replace(/\u001b\[\?25./g, '')
       .split('\u001b[2K')
+      .filter(x => x)
       .map(x => JSON.parse(x))
   );
 }
diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js
--- a/test/integration/options/watch.spec.js
+++ b/test/integration/options/watch.spec.js
@@ -47,6 +47,19 @@ describe('--watch', function() {
       });
     });
 
+    it('reruns test when watched test file crashes', function() {
+      const testFile = path.join(tempDir, 'test.js');
+      copyFixture(DEFAULT_FIXTURE, testFile);
+
+      replaceFileContents(testFile, 'done();', 'done((;');
+
+      return runMochaWatchJSONAsync([testFile], tempDir, () => {
+        replaceFileContents(testFile, 'done((;', 'done();');
+      }).then(results => {
+        expect(results, 'to have length', 1);
+      });
+    });
+
     describe('when in parallel mode', function() {
       it('reruns test when watched test file is touched', function() {
         const testFile = path.join(tempDir, 'test.js');
@@ -58,6 +71,19 @@ describe('--watch', function() {
           expect(results, 'to have length', 2);
         });
       });
+
+      it('reruns test when watched test file is crashed', function() {
+        const testFile = path.join(tempDir, 'test.js');
+        copyFixture(DEFAULT_FIXTURE, testFile);
+
+        replaceFileContents(testFile, 'done();', 'done((;');
+
+        return runMochaWatchJSONAsync([testFile], tempDir, () => {
+          replaceFileContents(testFile, 'done((;', 'done();');
+        }).then(results => {
+          expect(results, 'to have length', 1);
+        });
+      });
     });
 
     it('reruns test when file matching --watch-files changes', function() {