diff --git a/README.md b/README.md
index 32cc3c20..ff3e5e7f 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ expected to uphold this code.
     * [Sending cookies](#sending-cookies)
     * [Receiving cookies](#receiving-cookies)
     * [Obtaining the Response URL](#obtaining-the-response-url)
+    * [Aborting requests](#aborting-requests)
 * [Browser Support](#browser-support)
 
 ## Read this first
@@ -260,6 +261,36 @@ response.headers['X-Request-URL'] = request.url
 This server workaround is necessary if you need reliable `response.url` in
 Firefox < 32, Chrome < 37, Safari, or IE.
 
+#### Aborting requests
+
+This polyfill supports
+[the abortable fetch API](https://developers.google.com/web/updates/2017/09/abortable-fetch).
+However, aborting a fetch requires use of two additional DOM APIs:
+[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and
+[AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal).
+Typically, browsers that do not support fetch will also not support
+AbortController or AbortSignal. Consequently, you will need to include
+[an additional polyfill](https://github.com/mo/abortcontroller-polyfill#readme)
+for these APIs to abort fetches:
+
+```js
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
+import {fetch} from 'whatwg-fetch'
+
+const controller = new AbortController()
+
+fetch('/avatars', {
+  signal: controller.signal
+}).catch(function(ex) {
+  if (ex.name === 'AbortError') {
+    console.log('request aborted')
+  }
+})
+
+// some time later...
+controller.abort()
+```
+
 ## Browser Support
 
 - Chrome
diff --git a/fetch.js b/fetch.js
index 47b419d1..b684ff1b 100644
--- a/fetch.js
+++ b/fetch.js
@@ -321,6 +321,7 @@ export function Request(input, options) {
     }
     this.method = input.method
     this.mode = input.mode
+    this.signal = input.signal
     if (!body && input._bodyInit != null) {
       body = input._bodyInit
       input.bodyUsed = true
@@ -335,6 +336,7 @@ export function Request(input, options) {
   }
   this.method = normalizeMethod(options.method || this.method || 'GET')
   this.mode = options.mode || this.mode || null
+  this.signal = options.signal || this.signal
   this.referrer = null
 
   if ((this.method === 'GET' || this.method === 'HEAD') && body) {
@@ -422,11 +424,34 @@ Response.redirect = function(url, status) {
   return new Response(null, {status: status, headers: {location: url}})
 }
 
+export var DOMException = self.DOMException
+try {
+  new DOMException()
+} catch (err) {
+  DOMException = function(message, name) {
+    this.message = message
+    this.name = name
+    var error = Error(message)
+    this.stack = error.stack
+  }
+  DOMException.prototype = Object.create(Error.prototype)
+  DOMException.prototype.constructor = DOMException
+}
+
 export function fetch(input, init) {
   return new Promise(function(resolve, reject) {
     var request = new Request(input, init)
+
+    if (request.signal && request.signal.aborted) {
+      return reject(new DOMException('Aborted', 'AbortError'))
+    }
+
     var xhr = new XMLHttpRequest()
 
+    function abortXhr() {
+      xhr.abort()
+    }
+
     xhr.onload = function() {
       var options = {
         status: xhr.status,
@@ -446,6 +471,10 @@ export function fetch(input, init) {
       reject(new TypeError('Network request failed'))
     }
 
+    xhr.onabort = function() {
+      reject(new DOMException('Aborted', 'AbortError'))
+    }
+
     xhr.open(request.method, request.url, true)
 
     if (request.credentials === 'include') {
@@ -462,6 +491,17 @@ export function fetch(input, init) {
       xhr.setRequestHeader(name, value)
     })
 
+    if (request.signal) {
+      request.signal.addEventListener('abort', abortXhr)
+
+      xhr.onreadystatechange = function() {
+        // DONE (success or failure)
+        if (xhr.readyState === 4) {
+          request.signal.removeEventListener('abort', abortXhr)
+        }
+      }
+    }
+
     xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
   })
 }
diff --git a/package.json b/package.json
index 30ae630d..5153a7f4 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
   "repository": "github/fetch",
   "license": "MIT",
   "devDependencies": {
+    "abortcontroller-polyfill": "^1.1.9",
     "chai": "1.10.0",
     "eslint": "^4.19.1",
     "eslint-plugin-github": "^1.0.0",
diff --git a/script/server b/script/server
index 861a16d6..e89b9c91 100755
--- a/script/server
+++ b/script/server
@@ -80,6 +80,12 @@ var routes = {
     res.writeHead(204);
     res.end();
   },
+  '/slow': function(res) {
+    setTimeout(function() {
+      res.writeHead(200);
+      res.end();
+    }, 100);
+  },
   '/error': function(res) {
     res.destroy();
   },
diff --git a/test/test.html b/test/test.html
index 93de175c..eea0df32 100644
--- a/test/test.html
+++ b/test/test.html
@@ -33,6 +33,7 @@
   </script>
 
   <script src="/node_modules/promise-polyfill/promise.js"></script>
+  <script src="/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js"></script>
   <script src="/dist/fetch.umd.js"></script>
   <script src="/test/test.js"></script>
 
diff --git a/test/test.js b/test/test.js
index 3d5f5b37..1308c63c 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1048,6 +1048,142 @@ exercise.forEach(function(exerciseMode) {
         })
       })
 
+      suite('aborting', function() {
+        test('initially aborted signal', function() {
+          var controller = new AbortController()
+          controller.abort()
+
+          return fetch('/request', {
+            signal: controller.signal
+          }).then(
+            function() {
+              assert.ok(false)
+            },
+            function(error) {
+              assert.instanceOf(error, WHATWGFetch.DOMException)
+              assert.equal(error.name, 'AbortError')
+            }
+          )
+        })
+
+        test('initially aborted signal within Request', function() {
+          var controller = new AbortController()
+          controller.abort()
+
+          var request = new Request('/request', {signal: controller.signal})
+
+          return fetch(request).then(
+            function() {
+              assert.ok(false)
+            },
+            function(error) {
+              assert.equal(error.name, 'AbortError')
+            }
+          )
+        })
+
+        test('mid-request', function() {
+          var controller = new AbortController()
+
+          setTimeout(function() {
+            controller.abort()
+          }, 30)
+
+          return fetch('/slow', {
+            signal: controller.signal
+          }).then(
+            function() {
+              assert.ok(false)
+            },
+            function(error) {
+              assert.equal(error.name, 'AbortError')
+            }
+          )
+        })
+
+        test('mid-request within Request', function() {
+          var controller = new AbortController()
+          var request = new Request('/slow', {signal: controller.signal})
+
+          setTimeout(function() {
+            controller.abort()
+          }, 30)
+
+          return fetch(request).then(
+            function() {
+              assert.ok(false)
+            },
+            function(error) {
+              assert.equal(error.name, 'AbortError')
+            }
+          )
+        })
+
+        test('abort multiple with same signal', function() {
+          var controller = new AbortController()
+
+          setTimeout(function() {
+            controller.abort()
+          }, 30)
+
+          return Promise.all([
+            fetch('/slow', {
+              signal: controller.signal
+            }).then(
+              function() {
+                assert.ok(false)
+              },
+              function(error) {
+                assert.equal(error.name, 'AbortError')
+              }
+            ),
+            fetch('/slow', {
+              signal: controller.signal
+            }).then(
+              function() {
+                assert.ok(false)
+              },
+              function(error) {
+                assert.equal(error.name, 'AbortError')
+              }
+            )
+          ])
+        })
+
+        test('does not leak memory', function() {
+          var controller = new AbortController()
+          var signal = controller.signal
+
+          // success
+          return fetch('/request', {
+            signal: signal
+          })
+            .then(function() {
+              assert.deepEqual(signal.listeners['abort'], [])
+            })
+            .then(function() {
+              // failure
+              return fetch('/boom', {
+                signal: signal
+              }).catch(function() {
+                assert.deepEqual(signal.listeners['abort'], [])
+              })
+            })
+            .then(function() {
+              // aborted
+              setTimeout(function() {
+                signal.dispatchEvent({type: 'abort'})
+              }, 30)
+
+              return fetch('/slow', {
+                signal: signal
+              }).catch(function() {
+                assert.deepEqual(signal.listeners['abort'], [])
+              })
+            })
+        })
+      })
+
       suite('response', function() {
         test('populates body', function() {
           return fetch('/hello')
diff --git a/test/worker.js b/test/worker.js
index 7cfb5dea..5bc87c41 100644
--- a/test/worker.js
+++ b/test/worker.js
@@ -5,6 +5,7 @@ mocha.setup('tdd')
 self.assert = chai.assert
 
 importScripts('/node_modules/promise-polyfill/promise.js')
+importScripts('/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js')
 importScripts('/dist/fetch.umd.js')
 importScripts('/test/test.js')