diff --git a/docs/config/04-preprocessors.md b/docs/config/04-preprocessors.md
index e1d2a0dee..33b4edb00 100644
--- a/docs/config/04-preprocessors.md
+++ b/docs/config/04-preprocessors.md
@@ -93,3 +93,50 @@ return `false` and the preprocessor would not be executed on the CoffeeScript fi
 [custom plugins]: ../dev/plugins.html
 [plugins]: plugins.html
 [issue788]: https://github.com/karma-runner/karma/issues/788
+
+## Order of execution
+
+If a file matches only one key in the preprocessors config object, then karma
+will execute the preprocessors over that file in the order they are listed in
+the corresponding array.  So for instance if the config object is:
+
+```js
+preprocessors: {
+  '*.js': ['a', 'b']
+}
+```
+
+Then karma will execute `'a'` before executing `'b'`.
+
+If a file matches multiple keys, karma will do its best to execute the
+preprocessors in a reasonable order.  So if you have:
+
+```js
+preprocessors: {
+  '*.js': ['a', 'b'],
+  'a.*': ['b', 'c']
+}
+```
+
+then for `a.js`, karma will run `'a'` then `'b'` then `'c'`.  If two lists
+contradict eachother, like:
+
+```js
+preprocessors: {
+  '*.js': ['a', 'b'],
+  'a.*': ['b', 'a']
+}
+```
+
+then karma will arbitrarily pick one list to prioritize over the other.  In a
+case like:
+
+```js
+preprocessors: {
+  '*.js': ['a', 'b', 'c'],
+  'a.*': ['c', 'b', 'd']
+}
+```
+
+Then `'a'` will definitely be run first, `'d'` will definitely be run last, but
+it's arbitrarily if karma will run `'b'` before `'c'` or vice versa.
diff --git a/lib/preprocessor.js b/lib/preprocessor.js
index 8da50338e..668cb42ce 100644
--- a/lib/preprocessor.js
+++ b/lib/preprocessor.js
@@ -2,6 +2,7 @@ var fs = require('graceful-fs')
 var crypto = require('crypto')
 var mm = require('minimatch')
 var isBinaryFile = require('isbinaryfile')
+var combineLists = require('combine-lists')
 
 var log = require('./logger').create('preprocess')
 
@@ -64,9 +65,11 @@ var createPreprocessor = function (config, basePath, injector) {
     return p
   }
 
+  var allPreprocessors = []
   patterns.forEach(function (pattern) {
-    config[pattern].forEach(instantiatePreprocessor)
+    allPreprocessors = combineLists(allPreprocessors, config[pattern])
   })
+  allPreprocessors.forEach(instantiatePreprocessor)
 
   return function preprocess (file, done) {
     patterns = Object.keys(config)
@@ -81,36 +84,39 @@ var createPreprocessor = function (config, basePath, injector) {
           throw err
         }
 
-        var preprocessors = []
-        var nextPreprocessor = createNextProcessor(preprocessors, file, done)
-
+        var preprocessorNames = []
         for (var i = 0; i < patterns.length; i++) {
           if (mm(file.originalPath, patterns[i], {dot: true})) {
             if (thisFileIsBinary) {
               log.warn('Ignoring preprocessing (%s) %s because it is a binary file.',
                 config[patterns[i]].join(', '), file.originalPath)
             } else {
-              config[patterns[i]].forEach(function (name) {
-                var p = instances[name]
-                if (p == null) {
-                  p = instantiatePreprocessor(name)
-                }
-
-                if (p == null) {
-                  if (!alreadyDisplayedWarnings[name]) {
-                    alreadyDisplayedWarnings[name] = true
-                    log.warn('Failed to instantiate preprocessor %s', name)
-                  }
-                  return
-                }
-
-                instances[name] = p
-                preprocessors.push(p)
-              })
+              preprocessorNames = combineLists(preprocessorNames,
+                  config[patterns[i]])
             }
           }
         }
 
+        var preprocessors = []
+        var nextPreprocessor = createNextProcessor(preprocessors, file, done)
+        preprocessorNames.forEach(function (name) {
+          var p = instances[name]
+          if (p == null) {
+            p = instantiatePreprocessor(name)
+          }
+
+          if (p == null) {
+            if (!alreadyDisplayedWarnings[name]) {
+              alreadyDisplayedWarnings[name] = true
+              log.warn('Failed to instantiate preprocessor %s', name)
+            }
+            return
+          }
+
+          instances[name] = p
+          preprocessors.push(p)
+        })
+
         nextPreprocessor(null, thisFileIsBinary ? buffer : buffer.toString())
       })
     })
diff --git a/package.json b/package.json
index 1cc304873..be2a8a078 100644
--- a/package.json
+++ b/package.json
@@ -266,6 +266,7 @@
     "body-parser": "^1.12.4",
     "chokidar": "^1.4.1",
     "colors": "^1.1.0",
+    "combine-lists": "^1.0.0",
     "connect": "^3.3.5",
     "core-js": "^2.1.0",
     "di": "^0.0.1",
diff --git a/test/unit/preprocessor.spec.js b/test/unit/preprocessor.spec.js
index 59242f4c3..4833fb484 100644
--- a/test/unit/preprocessor.spec.js
+++ b/test/unit/preprocessor.spec.js
@@ -271,4 +271,54 @@ describe('preprocessor', () => {
       done()
     })
   })
+
+  it('should merge lists of preprocessors', (done) => {
+    var callOrder = [];
+    var fakePreprocessorA = sinon.spy((content, file, done) => {
+      callOrder.push('a')
+      done(null, content)
+    })
+    var fakePreprocessorB = sinon.spy((content, file, done) => {
+      callOrder.push('b')
+      done(null, content)
+    })
+    var fakePreprocessorC = sinon.spy((content, file, done) => {
+      callOrder.push('c')
+      done(null, content)
+    })
+    var fakePreprocessorD = sinon.spy((content, file, done) => {
+      callOrder.push('d')
+      done(null, content)
+    })
+
+    var injector = new di.Injector([{
+      'preprocessor:fakeA': ['factory', () => fakePreprocessorA],
+      'preprocessor:fakeB': ['factory', () => fakePreprocessorB],
+      'preprocessor:fakeC': ['factory', () => fakePreprocessorC],
+      'preprocessor:fakeD': ['factory', () => fakePreprocessorD]
+    }])
+
+    pp = m.createPreprocessor({
+      '/*/a.js': ['fakeA', 'fakeB'],
+      '/some/*': ['fakeB', 'fakeC'],
+      '/some/a.js': ['fakeD']
+    }, null, injector)
+
+    var file = {originalPath: '/some/a.js', path: 'path'}
+
+    pp(file, (err) => {
+      if (err) throw err
+
+      expect(fakePreprocessorA).to.have.been.called
+      expect(fakePreprocessorB).to.have.been.called
+      expect(fakePreprocessorC).to.have.been.called
+      expect(fakePreprocessorD).to.have.been.called
+
+      expect(callOrder.indexOf('d')).not.to.equal(-1)
+      expect(callOrder.filter((letter) => {
+        return letter != 'd'
+      })).to.eql(['a', 'b', 'c'])
+      done()
+    })
+  })
 })