diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,12 @@
 # Revision history for ghci-quickfix
 
+## 0.1.1.1 -- 2026-07-14
+* Sort errors in quickfix file by order of emission
+
 ## 0.1.1.0 -- 2026-05-13
 
 * Support GHC 9.14
 * Drop support for 9.6
-* Sort errors in quickfix file by order of emission
 
 ## 0.1.0.0 -- 2026-01-09
 
diff --git a/ghci-quickfix.cabal b/ghci-quickfix.cabal
--- a/ghci-quickfix.cabal
+++ b/ghci-quickfix.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ghci-quickfix
-version:            0.1.1.0
+version:            0.1.1.1
 synopsis:           GHC plugin that writes errors to a file for use with quickfix
 description:        GHC plugin that writes errors to a file for use with vim/nvim's quickfix system
 license:            BSD-3-Clause
@@ -31,11 +31,10 @@
                       ghc >= 9.8 && < 9.15,
                       text,
                       async >= 2.2 && < 3,
-                      stm-containers >= 1.2 && < 1.3,
                       directory,
                       stm,
-                      deferred-folds >= 0.9 && < 1,
-                      foldl >= 1 && < 2
+                      containers,
+                      safe-exceptions
     hs-source-dirs:   src
     default-language: GHC2021
 
@@ -62,6 +61,7 @@
 --     default-language: GHC2021
 --     hs-source-dirs:   play
 --     main-is:          Main.hs
+--     other-modules: Test
 --     build-depends:
 --         base,
 --         ghci-quickfix
diff --git a/src/GhciQuickfix.hs b/src/GhciQuickfix.hs
--- a/src/GhciQuickfix.hs
+++ b/src/GhciQuickfix.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE GADTs #-}
@@ -7,38 +6,57 @@
   , pluginOffByDefault
   ) where
 
-import           Control.Concurrent (threadDelay)
+import           Control.Concurrent (threadDelay, MVar, newEmptyMVar, tryPutMVar)
 import qualified Control.Concurrent.Async as Async
 import           Control.Concurrent.STM.TVar
-import           Control.Exception
-import qualified Control.Foldl as F
+import           Control.Exception.Safe
 import           Control.Monad
 import           Control.Monad.STM
 import qualified Data.Char as Char
 import           Data.Either (partitionEithers)
 import           Data.Foldable
+import           Data.Functor
 import           Data.IORef
 import qualified Data.List as List
+import           Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
 import           Data.Maybe
 import           Data.Monoid (First(..))
 import qualified Data.Ord as Ord
+import qualified Data.Set as Set
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
 import qualified Data.Text.IO as TIO
-import           Data.Traversable
-import qualified DeferredFolds.UnfoldlM as DF
-import qualified StmContainers.Map as SM
 import qualified System.Directory as Dir
 import qualified System.Environment as Env
+import           System.IO (stderr, hPutStrLn)
+import           System.IO.Unsafe (unsafePerformIO)
 
 import qualified GhciQuickfix.GhcFacade as Ghc
 
--- | STM Map is used to avoid contention between modules compiled in parrallel,
--- although it isn't clear if it actually helps since contention will still
--- happen with the STVars used in the same transaction.
--- The Int paired with the errors is used for ordering.
-type ErrMap = SM.Map FilePath (Int, [T.Text])
+type ErrMap = TVar (Map FilePath Errors)
 
+data Errors = Errors
+  { ordering :: !Int -- track the ordering of module compilation
+  , errors :: ![T.Text]
+  }
+
+{-# NOINLINE globalErrMap #-}
+globalErrMap :: ErrMap
+globalErrMap = unsafePerformIO (newTVarIO mempty)
+
+{-# NOINLINE globalErrsUpdated #-}
+globalErrsUpdated :: TVar Bool
+globalErrsUpdated = unsafePerformIO (newTVarIO False)
+
+{-# NOINLINE globalCounter #-}
+globalCounter :: IORef Int
+globalCounter = unsafePerformIO (newIORef 0)
+
+{-# NOINLINE writerThreadLock #-}
+writerThreadLock :: MVar ()
+writerThreadLock = unsafePerformIO newEmptyMVar
+
 plugin :: Ghc.Plugin
 plugin = Ghc.defaultPlugin
   { Ghc.driverPlugin = modifyHscEnv False
@@ -52,19 +70,24 @@
 
 -- | Background process that writes the quickfix file when errors change. Adds a
 -- delay to mitigate excessive IO.
-writeQuickfixLoop :: Maybe FilePath -> ErrMap -> TVar Bool -> IO ()
-writeQuickfixLoop mErrFilePath errMap updated = forever $ do
-    msgs <- atomically $ do
-      check =<< readTVar updated
-      writeTVar updated False
-      DF.foldM (F.generalize F.list) (SM.unfoldlM errMap)
-    prunedMsgs <- pruneDeletedFiles msgs errMap
-    TIO.writeFile (fromMaybe "errors.err" mErrFilePath)
-      . T.unlines . foldMap snd
-      -- Sort in reverse so that the first error in the list is the last one to
-      -- be emitted by the compiler
-      $ List.sortOn Ord.Down prunedMsgs
-    threadDelay 200_000 -- 200ms
+writeQuickfixLoop :: Maybe FilePath -> IO ()
+writeQuickfixLoop mErrFilePath = forever $
+  handleAny
+    (\err -> do
+      hPutStrLn stderr $ "ghci-quickfix: " <> displayException err
+      threadDelay 1_000_000
+    )
+    $ do
+      msgs <- atomically $ do
+        isUpdated <- swapTVar globalErrsUpdated False
+        check isUpdated
+        Map.toList <$> readTVar globalErrMap
+      prunedMsgs <- pruneDeletedFiles msgs
+      TIO.writeFile (fromMaybe "errors.err" mErrFilePath)
+        . T.unlines . foldMap' errors
+        -- sort so that the last compiled module is the first in the list
+        $ List.sortOn (Ord.Down . ordering) prunedMsgs
+      threadDelay 200_000 -- 200ms
 
 parseFilePathModifier :: [Ghc.CommandLineOption] -> IO (Either String [T.Text -> T.Text])
 parseFilePathModifier opts = do
@@ -88,7 +111,7 @@
 parseQuickfixFilePath :: [Ghc.CommandLineOption] -> IO (Maybe FilePath)
 parseQuickfixFilePath opts = do
   envPath <- Env.lookupEnv "GHCI_QUICKFIX_FILE"
-  pure $ getFirst $ foldMap (First . List.stripPrefix "--quickfix-file=") opts <> First envPath
+  pure $ getFirst $ foldMap' (First . List.stripPrefix "--quickfix-file=") opts <> First envPath
 
 parseIncludeParserErrors :: [Ghc.CommandLineOption] -> IO Bool
 parseIncludeParserErrors opts = do
@@ -109,17 +132,16 @@
     parseFilePathModifier opts >>= \case
       Left err -> fail err
       Right filePathMods -> do
-        errMap <- SM.newIO
-        errsUpdated <- newTVarIO False
-        counter <- newTVarIO 0
         quickfixFilePath <- parseQuickfixFilePath opts
-        void . Async.async $ writeQuickfixLoop quickfixFilePath errMap errsUpdated
+        started <- tryPutMVar writerThreadLock ()
+        when started $
+          void . Async.async $ writeQuickfixLoop quickfixFilePath
         includeParserErrors <- parseIncludeParserErrors opts
-        pure hscEnv { Ghc.hsc_hooks = modifyHooks includeParserErrors filePathMods (Ghc.hsc_hooks hscEnv) errMap errsUpdated counter }
+        pure hscEnv { Ghc.hsc_hooks = modifyHooks includeParserErrors filePathMods (Ghc.hsc_hooks hscEnv) }
   else
     pure hscEnv
   where
-    modifyHooks includeParserErrors filePathMods hooks (errMap :: ErrMap) (errsUpdated :: TVar Bool) (counter :: TVar Int) =
+    modifyHooks includeParserErrors filePathMods hooks =
       let runPhaseOrExistingHook :: Ghc.TPhase res -> IO res
           runPhaseOrExistingHook = maybe Ghc.runPhase (\(Ghc.PhaseHook h) -> h)
             $ Ghc.runPhaseHook hooks
@@ -132,7 +154,7 @@
             dsWarnVar <- newIORef mempty
             try (runPhaseOrExistingHook $ addDsLogHook (logHookHack dsWarnVar hscEnv) phase) >>= \case
               Left err@(Ghc.SourceError msgs) -> do
-                handleMessages includeParserErrors filePathMods errMap errsUpdated counter msgs
+                handleMessages includeParserErrors filePathMods msgs
                 throw err
               Right res -> do
                 dsWarns <- readIORef dsWarnVar
@@ -142,12 +164,12 @@
                     then atomically $ do
                       -- Module compiled without errors or warnings so delete map entry if exists
                       let modFile = Ghc.ms_hspp_file modSummary
-                      SM.lookup modFile errMap >>= \case
-                        Nothing -> pure ()
-                        Just _ -> do
-                          SM.delete (Ghc.ms_hspp_file modSummary) errMap
-                          writeTVar errsUpdated True
-                    else handleMessages includeParserErrors filePathMods errMap errsUpdated counter $
+                      entryDeleted <- stateTVar globalErrMap $ \m ->
+                        let (mOld, m') = Map.updateLookupWithKey (\_ _ -> Nothing) modFile m
+                        in (isJust mOld, m')
+                      when entryDeleted $
+                        writeTVar globalErrsUpdated True
+                    else handleMessages includeParserErrors filePathMods $
                       if length tcWarnings == length dsWarns
                       then tcWarnings -- has preferred formatting
                       else dsWarns
@@ -189,8 +211,8 @@
       line = Ghc.srcLocLine startLoc
       col = Ghc.srcLocCol startLoc
       truncateMsg txt =
-        let truncated = T.take 200 txt
-        in if T.length txt > 200 then truncated <> "…" else truncated
+        let truncated = T.take 201 txt
+        in if T.length truncated > 200 then T.dropEnd 1 truncated <> "…" else txt
       msg = truncateMsg . T.intercalate " • "
         $ T.unwords . T.words . T.pack
         . Ghc.renderWithContext ctx
@@ -205,40 +227,44 @@
 handleMessages
   :: Bool
   -> [T.Text -> T.Text]
-  -> ErrMap
-  -> TVar Bool
-  -> TVar Int
   -> Ghc.Messages Ghc.GhcMessage
   -> IO ()
-handleMessages includeParserErrors filePathMods errMap errsUpdated counter messages = do
+handleMessages includeParserErrors filePathMods messages = do
   let envelopes = Ghc.getMessages messages
       isParseError = \case
         Ghc.GhcPsMessage{} -> True
         _ -> False
       -- Filter out parse errors unless explicitly included
       errs = mapMaybe (formatDiagnostic filePathMods)
+             -- sort to match the order of appearance in GHCi
+           . List.sortOn
+               ( Ord.Down . fmap (\s -> (Ghc.srcSpanStartLine s, Ghc.srcSpanStartCol s))
+               . Ghc.srcSpanToRealSrcSpan . Ghc.errMsgSpan
+               )
            . filter (\env -> includeParserErrors || not (isParseError (Ghc.errMsgDiagnostic env)))
            $ Ghc.bagToList envelopes
       First mFile =
-        foldMap
+        foldMap'
           (First . fmap Ghc.unpackFS . Ghc.srcSpanFileName_maybe . Ghc.errMsgSpan)
-          $ Ghc.getMessages messages
-  for_ mFile $ \file -> atomically $ do
-    n <- stateTVar counter (\x -> let !nx = x + 1 in (x, nx))
-    SM.insert (n, errs) file errMap
-    writeTVar errsUpdated True
+          envelopes
+  for_ mFile $ \file ->
+    unless (null errs) $ do
+      n <- atomicModifyIORef' globalCounter (\x -> let !nx = x + 1 in (nx, x))
+      atomically $ do
+        modifyTVar' globalErrMap (Map.insert file (Errors n errs))
+        writeTVar globalErrsUpdated True
 
 -- | Remove errors for files that no longer exist
-pruneDeletedFiles :: [(FilePath, (Int, [T.Text]))] -> ErrMap -> IO [(Int, [T.Text])]
-pruneDeletedFiles errs errMap = do
+pruneDeletedFiles :: [(FilePath, Errors)] -> IO [Errors]
+pruneDeletedFiles errs = do
   let files = fst <$> errs
-  deletedFiles <- fmap catMaybes $
-    for files $ \file ->
-      Dir.doesFileExist file >>= \case
-        True -> pure Nothing
-        False -> pure (Just file)
-  atomically $ traverse_ (`SM.delete` errMap) deletedFiles
-  pure . fmap snd $ filter (not . (`elem` deletedFiles) . fst) errs
+  deletedFiles <-
+    (`foldMap'` files) $ \file ->
+      Dir.doesFileExist file <&> \case
+        True -> mempty
+        False -> Set.singleton file
+  atomically $ modifyTVar' globalErrMap (`Map.withoutKeys` deletedFiles)
+  pure . fmap snd $ filter ((`Set.notMember` deletedFiles) . fst) errs
 
 -- | Currently no good way to get warnings from desugarer, so a log action hook
 -- is used to get the raw SDoc. Note: unfortunately this will also capture
@@ -256,6 +282,6 @@
             diagOpts = Ghc.initDiagOpts $ Ghc.hsc_dflags hscEnv
             ghcMessage = Ghc.GhcDsMessage . Ghc.DsUnknownMessage $ Ghc.mkUnknownDiagnostic diag
             warn = Ghc.mkMsgEnvelope diagOpts srcSpan Ghc.neverQualify ghcMessage
-        modifyIORef dsWarnVar (Ghc.addMessage warn)
+        modifyIORef' dsWarnVar (Ghc.addMessage warn)
     _ -> pure ()
   logAction flags clss srcSpan sdoc
diff --git a/src/GhciQuickfix/GhcFacade.hs b/src/GhciQuickfix/GhcFacade.hs
--- a/src/GhciQuickfix/GhcFacade.hs
+++ b/src/GhciQuickfix/GhcFacade.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE CPP #-}
 module GhciQuickfix.GhcFacade
   ( module Ghc
   ) where
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -68,13 +68,14 @@
   void $ Proc.waitForProcess h
   hClose hDevNull
 
+  qfExists' <- Dir.doesFileExist qfFile
   -- Check that quickfix file was created and has expected contents
-  actualContents <- readFile qfFile
+  actualContents <- if qfExists' then readFile qfFile else pure ""
   expectedContents <- readFile $ qfFile ++ ".expected"
   assertEqual "Expected quickfix output" expectedContents actualContents
 
   -- Clean up
-  Dir.removeFile qfFile
+  when qfExists' (Dir.removeFile qfFile)
 
 runTestWarningFix :: Assertion
 runTestWarningFix = do
