diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,34 @@
+
+0.4
+---
+
+Misc changes:
+ * The package now uses and depends on `async` instead of using raw
+   `forkIO`.
+
+0.3
+---
+
+Package changes:
+ * Relaxed process lower bound to version 1.3.
+
+API changes:
+ * Added 'aspellDictionaries' to obtain the list of installed
+   dictionaries.
+
+Other changes:
+ * startAspell now gracefully handles Aspell startup failures by
+   reporting Aspell's standard error output in the event of a failed
+   startup. It also sanity-checks the Aspell pipe mode identification
+   string. (Fixes #2, #3.)
+
+
+0.2
+---
+
+* Relaxed base upper bound to < 5.
+
+0.1
+---
+
+Initial release.
diff --git a/ChangeLog.md b/ChangeLog.md
deleted file mode 100644
--- a/ChangeLog.md
+++ /dev/null
@@ -1,24 +0,0 @@
-
-0.3
----
-
-API changes:
- * Added 'aspellDictionaries' to obtain the list of installed
-   dictionaries.
-
-Other changes:
- * startAspell now gracefully handles Aspell startup failures by
-   reporting Aspell's standard error output in the event of a failed
-   startup. It also sanity-checks the Aspell pipe mode identification
-   string. (Fixes #2, #3.)
-
-
-0.2
----
-
-* Relaxed base upper bound to < 5.
-
-0.1
----
-
-Initial release.
diff --git a/aspell-pipe.cabal b/aspell-pipe.cabal
--- a/aspell-pipe.cabal
+++ b/aspell-pipe.cabal
@@ -1,5 +1,5 @@
 name:                aspell-pipe
-version:             0.3
+version:             0.4
 synopsis:            Pipe-based interface to the Aspell program
 description:         A pipe-based interface to the Aspell program (no
                      dynamic linking required).
@@ -10,8 +10,8 @@
 copyright:           2017 Jonathan Daugherty
 category:            Text
 build-type:          Simple
-extra-source-files:  ChangeLog.md
-cabal-version:       >=1.10
+extra-source-files:  CHANGELOG.md
+cabal-version:       1.18
 
 extra-doc-files:     README.md
 
@@ -25,6 +25,7 @@
 
   hs-source-dirs:      src
   default-language:    Haskell2010
-  build-depends:       base >=4.8 && <5,
+  build-depends:       base    >= 4.8 && < 5,
+                       async   >= 2.2 && < 2.3,
                        process >= 1.3,
                        text
diff --git a/src/Text/Aspell.hs b/src/Text/Aspell.hs
--- a/src/Text/Aspell.hs
+++ b/src/Text/Aspell.hs
@@ -24,8 +24,7 @@
 
 import qualified Control.Exception as E
 import Control.Monad (forM, when, void)
-import Control.Concurrent (forkIO, killThread)
-import Control.Concurrent.MVar (takeMVar, newEmptyMVar, putMVar)
+import qualified Control.Concurrent.Async as A
 import Data.Monoid ((<>))
 import Data.Maybe (fromJust)
 import Text.Read (readMaybe)
@@ -40,7 +39,7 @@
     Aspell { aspellProcessHandle  :: P.ProcessHandle
            , aspellStdin          :: Handle
            , aspellStdout         :: Handle
-           , aspellIdentification :: T.Text
+           , aspellIdentification :: T.Text -- ^ startup-reported version string
            }
 
 instance Show Aspell where
@@ -94,43 +93,23 @@
 
             (Just inH, Just outH, Just errH, ph) <- P.createProcess proc
 
-            -- Set up an mvar to hold the first available aspell output.
-            -- In this we store the first available stdout or stderr
-            -- line; if aspell dies immediately then we can expect a
-            -- stderr read (the error message), but on success we expect
-            -- a stdout read (the identification string). We fork two
-            -- threads: one to read stdout, one stderr. Whichever one
-            -- gets a result first tells us whether Aspell started
-            -- successfully. If the stderr thread wins, the stdout
-            -- thread will get an exception on hGetLine due to stdout
-            -- being closed, so we have to handle that. If the stdout
-            -- thread wins, the stderr thread will block forever and we
-            -- need to kill it.
-            initialResult <- newEmptyMVar
+            errorAsync <- A.async (T.hGetLine errH)
 
-            void $ forkIO $ do
-                identResult <- E.try $ T.hGetLine outH
-                case identResult of
-                    -- A failure means aspell died, so the stderr thread
-                    -- should have something to read.
-                    Left (_::E.SomeException) -> return ()
-                    Right ident -> putMVar initialResult $ Right ident
+            -- If startup is unsuccessful, stdout will close without output.
+            result <- E.try (T.hGetLine outH) :: IO (Either E.SomeException T.Text)
 
-            errThread <- forkIO $ do
-                err <- T.hGetLine errH
-                putMVar initialResult $ Left err
+            case result of
+                Left{} -> do
+                    e <- A.wait errorAsync
+                    fail ("Error starting aspell: " <> T.unpack e)
 
-            status <- takeMVar initialResult
-            case status of
-                Left e -> error $ "Error starting aspell: " <> show e
                 Right ident -> do
-                    killThread errThread
-
+                    A.cancel errorAsync
                     -- Now that aspell has started and we got an
                     -- identification string, we need to make sure it
                     -- looks legitimate before we proceed.
                     case validIdent ident of
-                        False -> error $ "Unexpected identification string: " <> show ident
+                        False -> fail ("Unexpected identification string: " <> T.unpack ident)
                         True -> do
                             let as = Aspell { aspellProcessHandle  = ph
                                             , aspellStdin          = inH
