Repository: jamesramsay/hercule
Pull request #202 (2016-03-16 02:35:58)

Standardise error handling (stream, async, sync)
Currently there aren't really any errors, only warnings. This is confusing because a dead link should cause an exception that can be handled by the consumer. Providing a logger is an obtuse and unhelpful way of surfacing what is very important feedback.

### Scope

This pull requests standardises error handling by API type:

- stream: `this.emit('error', err)`
- async: `callback(err, output)`
- sync: `throw err`

codecov-io: ## [Current coverage][1] is `96.42%`
> Merging **#202** into **next** will decrease coverage by **-3.58%** as of [`04ceebe`][3]


```diff
@@             next    #202   diff @@
=====================================
  Files           7       7       
  Stmts         227     224     -3
  Branches       23      26     +3
  Methods         0       0       
=====================================
- Hit           227     216    -11
- Partial         0       1     +1
- Missed          0       7     +7
```

> Review entire [Coverage Diff][4] as of [`04ceebe`][3]


[1]: https://codecov.io/github/jamesramsay/hercule?ref=04ceebe68796361c09778e01a7dcce40f1addc76
[2]: https://codecov.io/github/jamesramsay/hercule/features/suggestions?ref=04ceebe68796361c09778e01a7dcce40f1addc76
[3]: https://codecov.io/github/jamesramsay/hercule/commit/04ceebe68796361c09778e01a7dcce40f1addc76
[4]: https://codecov.io/github/jamesramsay/hercule/compare/4b94495796e7a9fe03a9920e2d94ecdab79b21aa...04ceebe68796361c09778e01a7dcce40f1addc76

> Powered by [Codecov](https://codecov.io). Updated on successful CI builds.

Patch:
diff --git a/README.md b/README.md
index d413ca9..916f8f5 100644
--- a/README.md
+++ b/README.md
@@ -211,6 +211,11 @@ import { TranscludeStream } from 'hercule';
 
 const trancluder = new TranscludeStream();
 
+transcluder.on('error', (err) => {
+  // Handle exceptions like dead links
+  console.log(err);
+});
+
 // assuming input is a readable stream and output is a writable stream
 input.pipe(transcluder).pipe(output);
 ```
@@ -243,6 +248,8 @@ __Examples__
 import { trancludeString } from 'hercule';
 
 trancludeString(':[foo](bar.md)', (err, output) => {
+  // Handle exceptions like dead links
+  if (err) console.log(err)
   console.log(output);
 });
 
@@ -252,9 +259,13 @@ trancludeString(':[foo](bar.md)', (err, output) => {
 // sync
 import { trancludeStringSync } from 'hercule';
 
-var output = trancludeFileSync('bar.md');
-console.log(output);
-
+try {
+  var output = trancludeFileSync('bar.md');
+  console.log(output);
+} catch (ex) {
+  // Handle exceptions like dead links
+  console.log(ex);
+}
 ```
 
 ---------------------------------------
@@ -285,6 +296,8 @@ __Examples__
 import { trancludeFile } from 'hercule';
 
 trancludeFileSync('foo.md', (err, output) => {
+  // Handle exceptions like dead links
+  if (err) console.log(err)
   console.log(output);
 });
 
@@ -294,6 +307,11 @@ trancludeFileSync('foo.md', (err, output) => {
 // sync
 import { trancludeFileSync } from 'hercule';
 
-var output = trancludeFileSync('foo.md');
-console.log(output);
+try {
+  var output = trancludeFileSync('foo.md');
+  console.log(output);
+} catch (ex) {
+  // Handle exceptions like dead links
+  console.log(ex);
+}
 ```
diff --git a/package.json b/package.json
index f2bc0ed..b3bf259 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,6 @@
   },
   "homepage": "https://github.com/jamesramsay/hercule",
   "dependencies": {
-    "bunyan": "^1.5.1",
     "dashdash": "^1.10.1",
     "duplexer2": "^0.1.4",
     "lodash": "^4.0.0",
diff --git a/src/config.js b/src/config.js
index de3b1d0..757df02 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,46 +1,31 @@
-import path from 'path';
 import _ from 'lodash';
 
-const APP_NAME = 'hercule';
-
 export const SUPPORTED_LINK_TYPES = [
   'string',
   'file',
   'http',
 ];
 
-// Log into the void, so that providing a logger is optional
-export const DEFAULT_LOG = {
-  error: () => null,
-};
-
 // Link detection (including leading whitespace)
+export const linkRegExp = new RegExp(/(^[\t ]*)?(\:\[.*?\]\((.*?)\))/gm);
+export const MATCH_GROUP = 0;
 export const WHITESPACE_GROUP = 1;
 export const PLACEHOLDER_GROUP = 2;
 export const LINK_GROUP = 3;
 
-export const linkRegExp = new RegExp(/(^[\t ]*)?(\:\[.*?\]\((.*?)\))/gm);
-export function linkMatch(match) {
-  return _.get(match, `[${LINK_GROUP}]`);
+export function defaultToken(match, options, whitespace) {
+  return {
+    content: match[MATCH_GROUP],
+    link: { href: _.isFunction(options.linkMatch) ? options.linkMatch(match) : match[LINK_GROUP] },
+    indent: _([whitespace, match[WHITESPACE_GROUP]]).filter(_.isString).value().join(''),
+    relativePath: options.relativePath,
+    references: options.references || [],
+    parents: options.parents || [],
+  };
 }
 
-export const BUNYAN_DEFAULTS = {
-  file: {
-    name: APP_NAME,
-    streams: [{
-      path: path.join(process.cwd(), `${APP_NAME}.log`),
-    }],
-  },
-  json: {
-    name: APP_NAME,
-    streams: [{
-      stream: process.stdout,
-    }],
-  },
-  ['json-err']: {
-    name: APP_NAME,
-    streams: [{
-      stream: process.stderr,
-    }],
-  },
-};
+export function defaultSeparator(match) {
+  return {
+    content: match,
+  };
+}
diff --git a/src/hercule.js b/src/hercule.js
index be2097e..b3343e2 100644
--- a/src/hercule.js
+++ b/src/hercule.js
@@ -5,22 +5,6 @@ import _ from 'lodash';
 import Transcluder from './transclude-stream';
 
 const SYNC_TIMEOUT = 10000;
-const LOG_OMIT = ['name', 'hostname', 'pid', 'time', 'v', 'msg'];
-const LOG_LEVELS = {
-  10: 'trace',
-  20: 'debug',
-  30: 'info',
-  40: 'warn',
-  50: 'error',
-};
-
-function relog(log, message) {
-  const msg = message.msg;
-  const level = message.level;
-  const body = _.omit(message, LOG_OMIT);
-
-  log[LOG_LEVELS[level]](body, msg);
-}
 
 export const TranscludeStream = Transcluder;
 
@@ -31,17 +15,17 @@ export function transcludeString(...args) {
 
   const transclude = new Transcluder(options, log, linkPaths);
   let outputString = '';
-
-  transclude.on('error', (err) => cb(err));
-
-  transclude.on('readable', function read() {
-    let content = null;
-    while ((content = this.read()) !== null) {
-      outputString += content.toString('utf8');
-    }
-  });
-
-  transclude.on('end', () => cb(null, outputString));
+  let cbErr = null;
+
+  transclude
+    .on('readable', function read() {
+      let content = null;
+      while ((content = this.read()) !== null) {
+        outputString += content.toString('utf8');
+      }
+    })
+    .on('error', (err) => (cbErr = err))
+    .on('end', () => cb(cbErr, outputString));
 
   transclude.write(input, 'utf8');
   transclude.end();
@@ -51,33 +35,30 @@ export function transcludeString(...args) {
 export function transcludeFile(...args) {
   const input = args.shift();
   const cb = args.pop();
-  const [options, log, linkPaths] = args;
+  const [options, linkPaths] = args;
 
-  const transclude = new Transcluder(options, log, linkPaths);
+  const transclude = new Transcluder(options, linkPaths);
   const inputStream = fs.createReadStream(input, { encoding: 'utf8' });
   let outputString = '';
+  let cbErr = null;
 
   inputStream.on('error', (err) => cb(err));
 
-  transclude.on('error', (err) => cb(err));
-
-  transclude.on('readable', function read() {
-    let content = null;
-    while ((content = this.read()) !== null) {
-      outputString += content;
-    }
-  });
-
-  transclude.on('end', () => cb(null, outputString));
+  transclude
+    .on('readable', function read() {
+      let content = null;
+      while ((content = this.read()) !== null) {
+        outputString += content;
+      }
+    })
+    .on('error', (err) => (cbErr = err))
+    .on('end', () => cb(cbErr, outputString));
 
   inputStream.pipe(transclude);
 }
 
 
-export function transcludeFileSync(...args) {
-  const input = args.shift();
-  const [options, log] = args;
-
+export function transcludeFileSync(input, options) {
   const syncOptions = { cwd: __dirname, timeout: SYNC_TIMEOUT };
   const syncArgs = [input, '--reporter', 'json-err'];
 
@@ -87,18 +68,15 @@ export function transcludeFileSync(...args) {
 
   const result = childProcess.spawnSync('../bin/hercule', syncArgs, syncOptions);
   const outputContent = result.stdout.toString();
-  const outputLogs = result.stderr.toString().split('\n');
+  const err = result.stderr.toString();
 
-  _.compact(outputLogs).map(JSON.parse).forEach((message) => relog(log, message));
+  if (err) throw new Error(JSON.parse(err).msg);
 
   return outputContent;
 }
 
 
-export function transcludeStringSync(...args) {
-  const input = args.shift();
-  const [options, log] = args;
-
+export function transcludeStringSync(input, options) {
   const syncOptions = { input, cwd: __dirname, timeout: SYNC_TIMEOUT };
   const syncArgs = ['--reporter', 'json-err'];
 
@@ -108,9 +86,9 @@ export function transcludeStringSync(...args) {
 
   const result = childProcess.spawnSync('../bin/hercule', syncArgs, syncOptions);
   const outputContent = result.stdout.toString();
-  const outputLogs = result.stderr.toString().split('\n');
+  const err = result.stderr.toString();
 
-  _.compact(outputLogs).map(JSON.parse).forEach((message) => relog(log, message));
+  if (err) throw new Error(JSON.parse(err).msg);
 
   return outputContent;
 }
diff --git a/src/inflate-stream.js b/src/inflate-stream.js
index a0e1eb7..e1e06aa 100644
--- a/src/inflate-stream.js
+++ b/src/inflate-stream.js
@@ -9,7 +9,7 @@ import regexpTokenizer from 'regexp-stream-tokenizer';
 import ResolveStream from './resolve-stream';
 import TrimStream from './trim-stream';
 import grammar from './transclude-parser';
-import { DEFAULT_LOG, linkRegExp, linkMatch, WHITESPACE_GROUP, SUPPORTED_LINK_TYPES } from './config';
+import { linkRegExp, defaultToken, defaultSeparator, WHITESPACE_GROUP, SUPPORTED_LINK_TYPES } from './config';
 
 /**
 * Input stream: object
@@ -30,36 +30,27 @@ const DEFAULT_OPTIONS = {
   output: 'content',
 };
 
-export default function InflateStream(opt, log = DEFAULT_LOG, linkPaths) {
+export default function InflateStream(opt, linkPaths) {
   const options = _.merge({}, DEFAULT_OPTIONS, opt);
 
   function inflateDuplex(chunk, link) {
-    const resolver = new ResolveStream(grammar, null, log, linkPaths);
-    const inflater = new InflateStream(null, log);
+    const resolver = new ResolveStream(grammar, null, linkPaths);
+    const inflater = new InflateStream();
     const trimmer = new TrimStream();
 
     function token(match) {
-      const tk = {
-        content: _.get(match, '[0]'),
-        link: {
-          href: _.isFunction(options.linkMatch) ? options.linkMatch(match) : linkMatch(match),
-        },
-        indent: _([chunk.indent, match[WHITESPACE_GROUP]]).filter(_.isString).value().join(''),
+      return _.merge(defaultToken(match, options, chunk.indent), {
         relativePath: path.dirname(link.href),
-        parents: [link.href, ...chunk.parents],
         references: [...chunk.references],
-      };
-      return tk;
+        parents: [link.href, ...chunk.parents],
+      });
+    }
+
+    function separator(match) {
+      return _.merge(defaultSeparator(match), { indent: chunk.indent });
     }
 
-    const tokenizerOptions = {
-      leaveBehind: `${WHITESPACE_GROUP}`,
-      token,
-      separator: (separator) => ({
-        content: separator,
-        indent: chunk.indent,
-      }),
-    };
+    const tokenizerOptions = { leaveBehind: `${WHITESPACE_GROUP}`, token, separator };
     const tokenizer = regexpTokenizer(tokenizerOptions, options.linkRegExp || linkRegExp);
 
     trimmer.pipe(tokenizer).pipe(resolver).pipe(inflater);
@@ -80,8 +71,11 @@ export default function InflateStream(opt, log = DEFAULT_LOG, linkPaths) {
     }
 
     if (_(parents).includes(link.href)) {
-      log.error({ link }, 'Circular dependency detected');
       this.push(chunk);
+      this.emit('error', {
+        msg: 'Circular dependency detected',
+        path: link.href,
+      });
       return cb();
     }
 
@@ -100,20 +94,31 @@ export default function InflateStream(opt, log = DEFAULT_LOG, linkPaths) {
     if (link.hrefType === 'file') input = fs.createReadStream(link.href, { encoding: 'utf8' });
     if (link.hrefType === 'http') input = request.get(link.href);
 
-    inflater.on('readable', function inputReadable() {
-      let content;
-      while ((content = this.read()) !== null) {
-        self.push(content);
-      }
-    });
-
-    inflater.on('end', () => cb());
-
-    input.on('error', (err) => {
-      log.error({ err, link }, 'Could not read file');
-      self.push(chunk);
-      return cb();
-    });
+    input
+      .on('error', (err) => {
+        this.push(chunk);
+        this.emit('error', _.merge(err, { msg: 'Could not read file' }));
+        cb();
+      })
+      .on('response', (res) => {
+        if (res.statusCode !== 200) {
+          this.emit('error', { msg: res.statusMessage, path: link.href });
+          cb();
+        }
+      });
+
+    inflater
+      .on('readable', function inputReadable() {
+        let content;
+        while ((content = this.read()) !== null) {
+          self.push(content);
+        }
+      })
+      .on('error', (err) => {
+        this.emit('error', err);
+        cb();
+      })
+      .on('end', () => cb());
 
     input.pipe(inflater);
   }
diff --git a/src/main.js b/src/main.js
index c333a7d..c98d9c3 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,11 +7,8 @@
 import fs from 'fs';
 import path from 'path';
 import dashdash from 'dashdash';
-import bunyan from 'bunyan';
 import Transcluder from './transclude-stream';
 
-import { BUNYAN_DEFAULTS } from './config';
-
 let opts;
 
 const parser = dashdash.createParser({
@@ -64,9 +61,6 @@ function main() {
     parentRefs: [],
   };
 
-  const bunyanOptions = BUNYAN_DEFAULTS[opts.reporter] || BUNYAN_DEFAULTS.file;
-  const log = bunyan.createLogger(bunyanOptions);
-
   if (opts._args.length === 0) {
     // Reading input from stdin
     inputStream = process.stdin;
@@ -86,7 +80,11 @@ function main() {
     outputStream = process.stdout;
   }
 
-  const transclude = new Transcluder(options, log);
+  const transclude = new Transcluder(options);
+
+  transclude.on('error', (err) => {
+    process.stderr.write(JSON.stringify(err));
+  });
 
   inputStream.pipe(transclude).pipe(outputStream);
 }
diff --git a/src/resolve-stream.js b/src/resolve-stream.js
index 0d700b7..1e7f7b6 100644
--- a/src/resolve-stream.js
+++ b/src/resolve-stream.js
@@ -2,8 +2,6 @@ import through2 from 'through2';
 import path from 'path';
 import _ from 'lodash';
 
-import { DEFAULT_LOG } from './config';
-
 /**
 * Input stream: (object)
 * - link (object, required)
@@ -26,7 +24,7 @@ const DEFAULT_OPTIONS = {
 };
 
 
-export default function ResolveStream(grammar, opt, log = DEFAULT_LOG, linkPaths = []) {
+export default function ResolveStream(grammar, opt, linkPaths = []) {
   const options = _.merge({}, DEFAULT_OPTIONS, opt);
 
 
@@ -63,8 +61,12 @@ export default function ResolveStream(grammar, opt, log = DEFAULT_LOG, linkPaths
     try {
       link = grammar.parse(rawLink);
     } catch (err) {
-      log.error({ err, link: rawLink }, 'Link could not be parsed');
       this.push(chunk);
+      this.emit('error', {
+        msg: 'Link could not be parsed',
+        path: rawLink,
+        error: err,
+      });
       return cb();
     }
 
diff --git a/src/transclude-stream.js b/src/transclude-stream.js
index df38b82..a4cea0f 100644
--- a/src/transclude-stream.js
+++ b/src/transclude-stream.js
@@ -7,7 +7,7 @@ import ResolveStream from './resolve-stream';
 import InflateStream from './inflate-stream';
 import IndentStream from './indent-stream';
 import grammar from './transclude-parser';
-import { linkRegExp, linkMatch, WHITESPACE_GROUP } from './config';
+import { linkRegExp, defaultToken, defaultSeparator, WHITESPACE_GROUP } from './config';
 
 /**
 * Input stream: string
@@ -20,35 +20,22 @@ const DEFAULT_OPTIONS = {
   output: 'content',
 };
 
-export default function Transcluder(opt, log, linkPaths) {
+export default function Transcluder(opt, linkPaths) {
   const options = _.merge({}, DEFAULT_OPTIONS, opt);
 
   function token(match) {
-    const tk = {
-      content: _.get(match, '[0]'),
-      link: {
-        href: _.isFunction(options.linkMatch) ? options.linkMatch(match) : linkMatch(match),
-      },
-      indent: _([options.indent, match[WHITESPACE_GROUP]]).filter(_.isString).value().join(''),
-      relativePath: options.relativePath,
-      references: options.references || [],
-      parents: options.parents || [],
-    };
-    return tk;
+    return defaultToken(match, options);
   }
 
-  const tokenizerOptions = {
-    leaveBehind: `${WHITESPACE_GROUP}`,
-    token,
-    separator: (separator) => ({
-      content: separator,
-      indent: options.indent,
-    }),
-  };
+  function separator(match) {
+    return defaultSeparator(match);
+  }
+
+  const tokenizerOptions = { leaveBehind: `${WHITESPACE_GROUP}`, token, separator };
   const tokenizer = regexpTokenizer(tokenizerOptions, options.linkRegExp || linkRegExp);
-  const resolver = new ResolveStream(grammar, null, log, linkPaths);
-  const inflater = new InflateStream({ linkRegExp: options.linkRegExp, linkMatch: options.linkMatch }, log, linkPaths);
-  const indenter = new IndentStream(null, log);
+  const resolver = new ResolveStream(grammar, null, linkPaths);
+  const inflater = new InflateStream({ linkRegExp: options.linkRegExp, linkMatch: options.linkMatch }, linkPaths);
+  const indenter = new IndentStream();
   const stringify = get('content');
 
   tokenizer
@@ -57,5 +44,17 @@ export default function Transcluder(opt, log, linkPaths) {
   .pipe(indenter)
   .pipe(stringify);
 
-  return duplexer(tokenizer, stringify);
+  const transcluder = duplexer(tokenizer, stringify);
+
+  resolver.on('error', (err) => {
+    transcluder.emit('error', err);
+    resolver.end();
+  });
+
+  inflater.on('error', (err) => {
+    transcluder.emit('error', err);
+    inflater.end();
+  });
+
+  return transcluder;
 }


Test patch:
diff --git a/test/fixtures/_aglio/_log.json b/test/fixtures/_aglio/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/_aglio/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/advanced/_log.json b/test/fixtures/advanced/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/advanced/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/circular-references/_expect.json b/test/fixtures/circular-references/_expect.json
new file mode 100644
index 0000000..7436169
--- /dev/null
+++ b/test/fixtures/circular-references/_expect.json
@@ -0,0 +1,7 @@
+{
+  "plan": 3,
+  "error": {
+    "msg": "Circular dependency detected",
+    "path": "fixtures/circular-references/fox.md"
+  }
+}
diff --git a/test/fixtures/circular-references/_log.json b/test/fixtures/circular-references/_log.json
deleted file mode 100644
index 4f319e8..0000000
--- a/test/fixtures/circular-references/_log.json
+++ /dev/null
@@ -1,12 +0,0 @@
-[
-  {
-    "name": "hercule",
-    "msg": "Circular dependency detected",
-    "link":
-    {
-      "hrefType": "file",
-      "href": "../fixtures/circular-references/fox.md"
-    },
-    "level": 50
-  }
-]
diff --git a/test/fixtures/escaped-quotes/_log.json b/test/fixtures/escaped-quotes/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/escaped-quotes/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/fallback-link/_log.json b/test/fixtures/fallback-link/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/fallback-link/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/http-deep-nesting/_log.json b/test/fixtures/http-deep-nesting/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/http-deep-nesting/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/http-link/_log.json b/test/fixtures/http-link/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/http-link/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/identical-links/_log.json b/test/fixtures/identical-links/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/identical-links/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/indentation/_log.json b/test/fixtures/indentation/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/indentation/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/index.js b/test/fixtures/index.js
index fd03808..0dfc709 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -4,11 +4,19 @@ var path = require('path');
 module.exports.fixtures = fs.readdirSync(__dirname).filter(function(file){
 	return (file[0] !== '_' && fs.statSync(path.join(__dirname, file)).isDirectory());
 }).map(function(file){
+	var expectedConfig = {};
+
+	try {
+		expectedConfig = JSON.parse(fs.readFileSync(__dirname + '/' + file + '/_expect.json', {encoding: 'utf8'}));
+	} catch (ex) {
+		expectedConfig = {};
+	}
+
 	return {
 		name: '' + file,
 		inputFile: __dirname + '/' + file + '/index.md',
 		input: fs.readFileSync(__dirname + '/' + file + '/index.md', {encoding: 'utf8'}),
     expectedOutput: fs.readFileSync(__dirname + '/' + file + '/_expect.md', {encoding: 'utf8'}),
-		expectedLogOutput: JSON.parse(fs.readFileSync(__dirname + '/' + file + '/_log.json', {encoding: 'utf8'}))
+		expectedConfig: expectedConfig
 	};
 });
diff --git a/test/fixtures/invalid-link-fallback/_log.json b/test/fixtures/invalid-link-fallback/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/invalid-link-fallback/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/invalid-link/_expect.json b/test/fixtures/invalid-link/_expect.json
new file mode 100644
index 0000000..fac0ae9
--- /dev/null
+++ b/test/fixtures/invalid-link/_expect.json
@@ -0,0 +1,7 @@
+{
+  "plan": 3,
+  "error": {
+    "msg": "Could not read file",
+    "path": "fixtures/invalid-link/i-dont-exist.md"
+  }
+}
diff --git a/test/fixtures/invalid-link/_expect.md b/test/fixtures/invalid-link/_expect.md
index bcba4e7..3af3a6a 100644
--- a/test/fixtures/invalid-link/_expect.md
+++ b/test/fixtures/invalid-link/_expect.md
@@ -1,1 +1,1 @@
-Jackdaws love my :[missing](i-dont-exist.md) sphinx of quartz.
+Jackdaws love my :[missing](i-dont-exist.md)
\ No newline at end of file
diff --git a/test/fixtures/invalid-link/_log.json b/test/fixtures/invalid-link/_log.json
deleted file mode 100644
index 5ac5f26..0000000
--- a/test/fixtures/invalid-link/_log.json
+++ /dev/null
@@ -1,12 +0,0 @@
-[
-  {
-    "name": "hercule",
-    "msg": "Could not read file",
-    "link":
-    {
-      "hrefType": "file",
-      "href": "../fixtures/invalid-link/i-dont-exist.md"
-    },
-    "level": 50
-  }
-]
diff --git a/test/fixtures/local-deep-nesting/_log.json b/test/fixtures/local-deep-nesting/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/local-deep-nesting/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/local-folder-nesting/_log.json b/test/fixtures/local-folder-nesting/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/local-folder-nesting/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/local-link/_log.json b/test/fixtures/local-link/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/local-link/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/no-link/_log.json b/test/fixtures/no-link/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/no-link/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/reference-collision/_log.json b/test/fixtures/reference-collision/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/reference-collision/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/references/_log.json b/test/fixtures/references/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/references/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/reset-references/_log.json b/test/fixtures/reset-references/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/reset-references/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/special-characters/_log.json b/test/fixtures/special-characters/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/special-characters/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/string-overrides/_log.json b/test/fixtures/string-overrides/_log.json
deleted file mode 100644
index fe51488..0000000
--- a/test/fixtures/string-overrides/_log.json
+++ /dev/null
@@ -1,1 +0,0 @@
-[]
diff --git a/test/fixtures/syntax-error/_expect.json b/test/fixtures/syntax-error/_expect.json
new file mode 100644
index 0000000..333aadf
--- /dev/null
+++ b/test/fixtures/syntax-error/_expect.json
@@ -0,0 +1,6 @@
+{
+  "error": {
+    "msg": "Link could not be parsed",
+    "path": "animal.md foo:bar:\"exception!\""
+  }
+}
diff --git a/test/fixtures/syntax-error/_expect.md b/test/fixtures/syntax-error/_expect.md
index 5630969..74889bc 100644
--- a/test/fixtures/syntax-error/_expect.md
+++ b/test/fixtures/syntax-error/_expect.md
@@ -1,1 +1,1 @@
-The quick brown :[](animal.md foo:bar:"exception!") jumps over the lazy dog.
+The quick brown :[](animal.md foo:bar:"exception!")
\ No newline at end of file
diff --git a/test/fixtures/syntax-error/_log.json b/test/fixtures/syntax-error/_log.json
deleted file mode 100644
index a70c8c1..0000000
--- a/test/fixtures/syntax-error/_log.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[
-  {
-    "name": "hercule",
-    "link": "animal.md foo:bar:\"exception!\"",
-    "msg": "Link could not be parsed",
-    "level": 50
-  }
-]
diff --git a/test/integration/transcludeFile.js b/test/integration/transcludeFile.js
index 00ca453..ca8d754 100644
--- a/test/integration/transcludeFile.js
+++ b/test/integration/transcludeFile.js
@@ -8,13 +8,20 @@ import './_mock';
 
 _.forEach((fixtures.fixtures), (fixture) => {
   test.cb(`should transclude ${fixture.name}`, (t) => {
-    const options = {
-      relativePath: path.resolve(__dirname, '../fixtures', fixture.name),
-    };
+    const options = { relativePath: path.resolve(__dirname, '../fixtures', fixture.name) };
+    const config = fixture.expectedConfig;
+
+    // TODO: ava plan
+    // if (fixture.expectedConfig.plan > 0) t.plan(fixture.expectedConfig.plan);
 
     transcludeFile(fixture.inputFile, options, (err, output) => {
-      t.same(err, null);
-      t.same(output, fixture.expectedOutput);
+      if (err) {
+        t.same(err.msg, config.error.msg);
+        t.regex(err.path, new RegExp(config.error.path));
+      } else {
+        t.same(err, null);
+        t.same(output, fixture.expectedOutput);
+      }
       t.end();
     });
   });
diff --git a/test/integration/transcludeFileSync.js b/test/integration/transcludeFileSync.js
index ba2bc47..faa1aa1 100644
--- a/test/integration/transcludeFileSync.js
+++ b/test/integration/transcludeFileSync.js
@@ -1,8 +1,5 @@
 import test from 'ava';
-import path from 'path';
 import _ from 'lodash';
-import through2 from 'through2';
-import bunyan from 'bunyan';
 
 import { transcludeFileSync } from '../../lib/hercule';
 import fixtures from '../fixtures';
@@ -16,40 +13,21 @@ if (major < 1 && minor < 12) {
   });
 }
 
-test.beforeEach('prepare log monitor', (t) => {
-  t.context.logOutput = [];
-  t.context.logStream = through2.obj();
-
-  t.context.log = bunyan.createLogger({
-    name: 'hercule',
-    streams: [{
-      stream: t.context.logStream,
-    }],
-  });
-
-  t.context.logStream.on('readable', function read() {
-    let message = null;
-    while ((message = this.read()) !== null) {
-      message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
-
-      // Make paths relatice to process for testing purposes
-      if (message.link && message.link.href) {
-        message.link.href = path.relative(process.cwd(), message.link.href);
-      }
-
-      t.context.logOutput.push(message);
-    }
-  });
-});
-
-
 _.forEach((fixtures.fixtures), (fixture) => {
   // Exclude http tests because mocking won't cover sync sub-process
   if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
 
   test(`should transclude ${fixture.name}`, (t) => {
-    const output = transcludeFileSync(fixture.inputFile, {}, t.context.log);
-    t.same(output, fixture.expectedOutput);
-    t.same(t.context.logOutput, fixture.expectedLogOutput);
+    const config = fixture.expectedConfig;
+    try {
+      const output = transcludeFileSync(fixture.inputFile);
+      t.same(output, fixture.expectedOutput);
+    } catch (ex) {
+      if (config.error) {
+        t.same(ex.message, config.error.msg);
+      } else {
+        t.fail();
+      }
+    }
   });
 });
diff --git a/test/integration/transcludeStream.js b/test/integration/transcludeStream.js
index a197952..d319bb2 100644
--- a/test/integration/transcludeStream.js
+++ b/test/integration/transcludeStream.js
@@ -2,62 +2,36 @@ import test from 'ava';
 import _ from 'lodash';
 import fs from 'fs';
 import path from 'path';
-import through2 from 'through2';
-import bunyan from 'bunyan';
 
 import { TranscludeStream } from '../../lib/hercule';
 import fixtures from '../fixtures';
 import './_mock';
 
-
-test.beforeEach('prepare log monitor', (t) => {
-  t.context.logOutput = [];
-  t.context.logStream = through2.obj();
-
-  t.context.log = bunyan.createLogger({
-    name: 'hercule',
-    streams: [{
-      stream: t.context.logStream,
-    }],
-  });
-
-  t.context.logStream.on('readable', function read() {
-    let message = null;
-    while ((message = this.read()) !== null) {
-      message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
-
-      // Make paths relatice to process for testing purposes
-      if (message.link && message.link.href) {
-        message.link.href = path.relative(process.cwd(), message.link.href);
-      }
-
-      t.context.logOutput.push(message);
-    }
-  });
-});
-
-
 _.forEach((fixtures.fixtures), (fixture) => {
   test.cb(`should transclude ${fixture.name}`, (t) => {
     const input = fs.createReadStream(fixture.inputFile, { encoding: 'utf8' });
-    const options = {
-      relativePath: path.dirname(fixture.inputFile),
-    };
+    const options = { relativePath: path.dirname(fixture.inputFile) };
     const transclude = new TranscludeStream(options, t.context.log);
+    const config = fixture.expectedConfig;
     let outputString = '';
 
-    transclude.on('readable', function read() {
-      let content = null;
-      while ((content = this.read()) !== null) {
-        outputString += content;
-      }
-    });
-
-    transclude.on('end', () => {
-      t.same(outputString, fixture.expectedOutput);
-      t.same(t.context.logOutput, fixture.expectedLogOutput);
-      t.end();
-    });
+    if (fixture.expectedConfig.plan > 0) t.plan(fixture.expectedConfig.plan);
+    transclude
+      .on('readable', function read() {
+        let content = null;
+        while ((content = this.read()) !== null) {
+          outputString += content;
+        }
+      })
+      .on('error', (err) => {
+        // All errors should include a message and path
+        t.same(err.msg, config.error.msg);
+        t.regex(err.path, new RegExp(config.error.path));
+      })
+      .on('end', () => {
+        t.same(outputString, fixture.expectedOutput);
+        t.end();
+      });
 
     input.pipe(transclude);
   });
diff --git a/test/integration/transcludeString.js b/test/integration/transcludeString.js
index 6ec3602..09efb19 100644
--- a/test/integration/transcludeString.js
+++ b/test/integration/transcludeString.js
@@ -9,13 +9,20 @@ import './_mock';
 
 _.forEach((fixtures.fixtures), (fixture) => {
   test.cb(`should transclude ${fixture.name}`, (t) => {
-    const options = {
-      relativePath: path.resolve(__dirname, '../fixtures', fixture.name),
-    };
+    const options = { relativePath: path.resolve(__dirname, '../fixtures', fixture.name) };
+    const config = fixture.expectedConfig;
+
+    // TODO: ava plan
+    // if (fixture.expectedConfig.plan > 0) t.plan(fixture.expectedConfig.plan);
 
     transcludeString(fixture.input, options, (err, output) => {
-      t.same(err, null);
-      t.same(output, fixture.expectedOutput);
+      if (err) {
+        t.same(err.msg, config.error.msg);
+        t.regex(err.path, new RegExp(config.error.path));
+      } else {
+        t.same(err, null);
+        t.same(output, fixture.expectedOutput);
+      }
       t.end();
     });
   });
diff --git a/test/integration/transcludeStringSync.js b/test/integration/transcludeStringSync.js
index f6c312b..fe4a26d 100644
--- a/test/integration/transcludeStringSync.js
+++ b/test/integration/transcludeStringSync.js
@@ -1,8 +1,6 @@
 import test from 'ava';
 import path from 'path';
 import _ from 'lodash';
-import through2 from 'through2';
-import bunyan from 'bunyan';
 
 import { transcludeStringSync } from '../../lib/hercule';
 import fixtures from '../fixtures';
@@ -16,43 +14,22 @@ if (major < 1 && minor < 12) {
   });
 }
 
-test.beforeEach('prepare log monitor', (t) => {
-  t.context.logOutput = [];
-  t.context.logStream = through2.obj();
-
-  t.context.log = bunyan.createLogger({
-    name: 'hercule',
-    streams: [{
-      stream: t.context.logStream,
-    }],
-  });
-
-  t.context.logStream.on('readable', function read() {
-    let message = null;
-    while ((message = this.read()) !== null) {
-      message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
-
-      // Make paths relatice to process for testing purposes
-      if (message.link && message.link.href) {
-        message.link.href = path.relative(process.cwd(), message.link.href);
-      }
-
-      t.context.logOutput.push(message);
-    }
-  });
-});
-
-
 _.forEach((fixtures.fixtures), (fixture) => {
   // Exclude http tests because mocking won't cover sub-process
   if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
 
   test(`should transclude ${fixture.name}`, (t) => {
-    const options = {
-      relativePath: path.resolve(__dirname, '../fixtures', fixture.name),
-    };
-    const output = transcludeStringSync(fixture.input, options, t.context.log);
-    t.same(output, fixture.expectedOutput);
-    t.same(t.context.logOutput, fixture.expectedLogOutput);
+    const options = { relativePath: path.resolve(__dirname, '../fixtures', fixture.name) };
+    const config = fixture.expectedConfig;
+    try {
+      const output = transcludeStringSync(fixture.input, options);
+      t.same(output, fixture.expectedOutput);
+    } catch (ex) {
+      if (config.error) {
+        t.same(ex.message, config.error.msg);
+      } else {
+        t.fail();
+      }
+    }
   });
 });
diff --git a/test/units/inflate-stream.js b/test/units/inflate-stream.js
index 6e83609..3abe2fb 100644
--- a/test/units/inflate-stream.js
+++ b/test/units/inflate-stream.js
@@ -6,37 +6,37 @@ import InflateStream from '../../lib/inflate-stream';
 
 test.cb('should handle no input', (t) => {
   const testStream = new InflateStream();
-
-  testStream.on('readable', function read() {
-    if (this.read() !== null) t.fail();
-  });
-
-  testStream.on('end', () => {
-    t.pass();
-    t.end();
-  });
+  testStream
+    .on('readable', function read() {
+      if (this.read() !== null) t.fail();
+    })
+    .on('error', () => t.fail())
+    .on('end', () => {
+      t.pass();
+      t.end();
+    });
 
   testStream.end();
 });
 
 
 test.cb('should skip input without link', (t) => {
-  const input = {
-    content: 'The quick brown fox jumps over the lazy dog./n',
-  };
+  const input = { content: 'The quick brown fox jumps over the lazy dog./n' };
   const testStream = new InflateStream();
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.notOk(chunk.link);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.pass();
-    t.end();
-  });
+  t.plan(2);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.notOk(chunk.link);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => {
+      t.pass();
+      t.end();
+    });
 
   testStream.write(input);
   testStream.end();
@@ -57,24 +57,25 @@ test.cb('should inflate input with file link', (t) => {
   const testStream = new InflateStream();
   let output = '';
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      output += chunk.content;
-    }
-  });
-
-  testStream.on('end', () => {
-    t.same(output, expected);
-    t.end();
-  });
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        output += chunk.content;
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => {
+      t.same(output, expected);
+      t.end();
+    });
 
   testStream.write(input);
   testStream.end();
 });
 
 
-test.cb('should skip input with invalid file link', (t) => {
+test.cb('should emit error with invalid file link', (t) => {
   const input = {
     content: ':[Example](size.md)',
     link: {
@@ -84,19 +85,21 @@ test.cb('should skip input with invalid file link', (t) => {
     parents: [],
     references: [],
   };
-  const expected = ':[Example](size.md)';
   const testStream = new InflateStream();
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(3);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.content, input.content);
+      }
+    })
+    .on('error', (err) => {
+      t.same(err.msg, 'Could not read file');
+      t.regex(err.path, /i-dont-exist\.md/);
+    })
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -114,16 +117,15 @@ test.cb('should inflate input with string link', (t) => {
   const expected = 'tiny';
   const testStream = new InflateStream();
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.content, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -145,23 +147,22 @@ test.cb('should inflate input with http link', (t) => {
 
   nock('http://github.com').get('/size.md').reply(200, 'big\n');
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.content, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
 });
 
 
-test.cb('should skip input with invalid http link', (t) => {
+test.cb('should emit error with invalid http link', (t) => {
   const input = {
     content: ':[Example](size.md)',
     link: {
@@ -171,21 +172,21 @@ test.cb('should skip input with invalid http link', (t) => {
     parents: [],
     references: [],
   };
-  const expected = ':[Example](size.md)';
   const testStream = new InflateStream();
 
   nock('http://github.com').get('/i-dont-exist.md').reply(404);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      if (this.read() !== null) t.fail();
+    })
+    .on('error', (err) => {
+      // TODO: Requires https://github.com/pgte/nock/issues/469
+      // t.same(err.msg, 'Circular dependency detected');
+      t.regex(err.path, /i-dont-exist\.md/);
+    })
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -203,23 +204,22 @@ test.cb('should not make modifications if hrefType is unrecognised', (t) => {
   const expected = ':[Example](size.md)';
   const testStream = new InflateStream();
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.content, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
 });
 
 
-test.cb('should skip circular references', (t) => {
+test.cb('should emit error on circular references', (t) => {
   const input = {
     content: ':[Example](size.md)',
     link: {
@@ -231,16 +231,19 @@ test.cb('should skip circular references', (t) => {
   const expected = ':[Example](size.md)';
   const testStream = new InflateStream();
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.content, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(3);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.content, expected);
+      }
+    })
+    .on('error', (err) => {
+      t.same(err.msg, 'Circular dependency detected');
+      t.same(err.path, 'size.md');
+    })
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
diff --git a/test/units/resolve-stream.js b/test/units/resolve-stream.js
index 7622549..697220d 100644
--- a/test/units/resolve-stream.js
+++ b/test/units/resolve-stream.js
@@ -6,36 +6,35 @@ import ResolveStream from '../../lib/resolve-stream';
 test.cb('should handle no input', (t) => {
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    if (this.read() !== null) t.fail();
-  });
-
-  testStream.on('end', () => {
-    t.pass();
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      if (this.read() !== null) t.fail();
+    })
+    .on('error', () => t.fail())
+    .on('end', () => {
+      t.pass();
+      t.end();
+    });
 
   testStream.end();
 });
 
 
 test.cb('should skip input without link', (t) => {
-  const input = {
-    content: 'The quick brown fox jumps over the lazy dog./n',
-  };
+  const input = { content: 'The quick brown fox jumps over the lazy dog./n' };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.notOk(chunk.link);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.pass();
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.notOk(chunk.link);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -55,17 +54,16 @@ test.cb('should parse input simple link', (t) => {
   };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.link, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.pass();
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.link, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -103,16 +101,16 @@ test.cb('should parse input with overrides', (t) => {
   ];
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.references, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.references, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -144,16 +142,16 @@ test.cb('should parse input with overriding link', (t) => {
   };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk.link, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk.link, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -188,23 +186,23 @@ test.cb('should parse input with fallback link', (t) => {
   };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
 });
 
 
-test.cb('should handle parse error', (t) => {
+test.cb('should emit error on invalid transclusion link', (t) => {
   const input = {
     content: ':[](animal.md foo:bar:"exception!")',
     link: {
@@ -213,16 +211,16 @@ test.cb('should handle parse error', (t) => {
   };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk, input);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(2);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk, input);
+      }
+    })
+    .on('error', () => t.pass())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
@@ -248,16 +246,16 @@ test.cb('should resolve link relative to file', (t) => {
   };
   const testStream = new ResolveStream(grammar);
 
-  testStream.on('readable', function read() {
-    let chunk = null;
-    while ((chunk = this.read()) !== null) {
-      t.same(chunk, expected);
-    }
-  });
-
-  testStream.on('end', () => {
-    t.end();
-  });
+  t.plan(1);
+  testStream
+    .on('readable', function read() {
+      let chunk = null;
+      while ((chunk = this.read()) !== null) {
+        t.same(chunk, expected);
+      }
+    })
+    .on('error', () => t.fail())
+    .on('end', () => t.end());
 
   testStream.write(input);
   testStream.end();
diff --git a/test/units/transcludeFile.js b/test/units/transcludeFile.js
index 787dadc..1441612 100644
--- a/test/units/transcludeFile.js
+++ b/test/units/transcludeFile.js
@@ -24,36 +24,36 @@ test.cb('should transclude with optional relativePath argument', (t) => {
   });
 });
 
-test.cb('should transclude with optional log handler', (t) => {
-  const input = path.join(__dirname, '../fixtures/no-link/index.md');
-  const expected = 'The quick brown fox jumps over the lazy dog.\n';
-  const logger = {
-    error: () => t.fail(),
-    warn: () => t.fail(),
-  };
-  transcludeFile(input, null, logger, (err, output) => {
-    t.same(err, null);
-    t.same(output, expected);
-    t.end();
-  });
-});
-
 test.cb('should return error if file doesn\'t exist', (t) => {
   const input = path.join('i-dont-exist.md');
-  transcludeFile(input, null, null, (err, output) => {
+  transcludeFile(input, (err, output) => {
     t.ok(err);
+    // t.same(err.msg, 'TODO');
+    t.same(err.path, 'i-dont-exist.md');
     t.notOk(output);
     t.end();
   });
 });
 
+// test.cb('should return error if circular dependency found', (t) => {
+//   const input = path.join(__dirname, '../fixtures/circular-references/index.md');
+//   const options = { relativePath: path.join(__dirname, '../fixtures/circular-references') };
+//   transcludeFile(input, options, (err, output) => {
+//     t.ok(err);
+//     t.same(err.msg, 'Circular dependency detected');
+//     t.regex(err.path, /fixtures\/circular-references\/fox.md/);
+//     t.notOk(output);
+//     t.end();
+//   });
+// });
+
 test.cb('should provide pathList if variable provided', (t) => {
   const input = path.join(__dirname, '../fixtures/local-link/index.md');
   const options = { relativePath: path.join(__dirname, '../fixtures/local-link') };
   const expected = 'Jackdaws love my big sphinx of quartz.\n';
   const pathList = [];
 
-  transcludeFile(input, options, null, pathList, (err, output) => {
+  transcludeFile(input, options, pathList, (err, output) => {
     t.same(err, null);
     t.same(output, expected);
     t.regex(pathList[0], /fixtures\/local-link\/size\.md/);
diff --git a/test/units/transcludeFileSync.js b/test/units/transcludeFileSync.js
index 197a4e6..e7e61a8 100644
--- a/test/units/transcludeFileSync.js
+++ b/test/units/transcludeFileSync.js
@@ -26,13 +26,12 @@ test('should transclude with optional relativePath argument', (t) => {
   t.same(output, expected);
 });
 
-test('should transclude with optional log handler', (t) => {
+test('should throw error with invalid link', (t) => {
   const input = path.join(__dirname, '../fixtures/invalid-link/index.md');
-  const expected = 'Jackdaws love my :[missing](i-dont-exist.md) sphinx of quartz.\n';
-  const logger = {
-    error: () => t.pass(),
-    warn: () => t.pass(),
-  };
-  const output = transcludeFileSync(input, null, logger);
-  t.same(output, expected);
+  try {
+    transcludeFileSync(input);
+    t.fail();
+  } catch (ex) {
+    t.same(ex.message, 'Could not read file');
+  }
 });
diff --git a/test/units/transcludeString.js b/test/units/transcludeString.js
index e92cd46..6700ba7 100644
--- a/test/units/transcludeString.js
+++ b/test/units/transcludeString.js
@@ -23,26 +23,12 @@ test.cb('should transclude with optional relativePath argument', (t) => {
   });
 });
 
-test.cb('should transclude with optional log handler', (t) => {
-  const input = 'The quick brown fox jumps over the lazy dog.';
-  const expected = 'The quick brown fox jumps over the lazy dog.';
-  const logger = {
-    error: () => t.fail(),
-    warn: () => t.fail(),
-  };
-  transcludeString(input, null, logger, (err, output) => {
-    t.same(err, null);
-    t.same(output, expected);
-    t.end();
-  });
-});
-
 test.cb('should provide pathList if variable provided', (t) => {
   const input = 'The quick brown fox jumps over the lazy dog.';
   const expected = 'The quick brown fox jumps over the lazy dog.';
   const pathList = [];
 
-  transcludeString(input, null, null, pathList, (err, output) => {
+  transcludeString(input, null, pathList, (err, output) => {
     t.same(err, null);
     t.same(output, expected);
     t.same(pathList.length, 0);
diff --git a/test/units/transcludeStringSync.js b/test/units/transcludeStringSync.js
index c3fa7a5..823ed0f 100644
--- a/test/units/transcludeStringSync.js
+++ b/test/units/transcludeStringSync.js
@@ -25,13 +25,12 @@ test('should transclude with optional relativePath argument', (t) => {
   t.same(output, expected);
 });
 
-test('should transclude with optional log handler', (t) => {
+test('should throw error with invalid link', (t) => {
   const input = 'Jackdaws love my :[missing](i-dont-exist.md) sphinx of quartz.';
-  const expected = 'Jackdaws love my :[missing](i-dont-exist.md) sphinx of quartz.';
-  const logger = {
-    error: () => t.pass(),
-    warn: () => t.pass(),
-  };
-  const output = transcludeStringSync(input, null, logger);
-  t.same(output, expected);
+  try {
+    transcludeStringSync(input);
+    t.fail();
+  } catch (ex) {
+    t.same(ex.message, 'Could not read file');
+  }
 });