diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,9 @@
 Changelog for Shake (* = breaking change)
 
+0.17.1, released 2018-11-14
+    Add actionCatch, to catch exceptions in Action
+    #622, avoid potential maxBound overflow in batch
+    Reduce the context required for addBuiltinRule
 0.17, released 2018-10-15
     Add Database module for repeated execution from one database
 *   Don't export the body of the Typeable type class
diff --git a/shake.cabal b/shake.cabal
--- a/shake.cabal
+++ b/shake.cabal
@@ -1,7 +1,7 @@
 cabal-version:      >= 1.18
 build-type:         Simple
 name:               shake
-version:            0.17
+version:            0.17.1
 license:            BSD3
 license-file:       LICENSE
 category:           Development, Shake
@@ -30,7 +30,7 @@
     (e.g. compiler version).
 homepage:           https://shakebuild.com
 bug-reports:        https://github.com/ndmitchell/shake/issues
-tested-with:        GHC==8.6.1, GHC==8.4.3, GHC==8.2.2, GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2
+tested-with:        GHC==8.6.1, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2
 extra-doc-files:
     CHANGES.txt
     README.md
diff --git a/src/Development/Shake.hs b/src/Development/Shake.hs
--- a/src/Development/Shake.hs
+++ b/src/Development/Shake.hs
@@ -54,7 +54,7 @@
     shakeOptions,
     Rules, action, withoutActions, alternatives, priority, versioned,
     Action, traced,
-    liftIO, actionOnException, actionFinally, actionRetry, runAfter,
+    liftIO, actionOnException, actionFinally, actionCatch, actionRetry, runAfter,
     ShakeException(..),
     -- * Configuration
     ShakeOptions(..), Rebuild(..), Lint(..), Change(..),
diff --git a/src/Development/Shake/Internal/Core/Action.hs b/src/Development/Shake/Internal/Core/Action.hs
--- a/src/Development/Shake/Internal/Core/Action.hs
+++ b/src/Development/Shake/Internal/Core/Action.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE RecordWildCards, NamedFieldPuns, ScopedTypeVariables, ConstraintKinds, TupleSections, ViewPatterns #-}
 
 module Development.Shake.Internal.Core.Action(
-    actionOnException, actionFinally, actionRetry,
+    actionOnException, actionFinally, actionCatch, actionRetry,
     getShakeOptions, getProgress, runAfter,
     lintTrackRead, lintTrackWrite, lintTrackAllow,
     getVerbosity, putWhen, putLoud, putNormal, putQuiet, withVerbosity, quietly,
@@ -102,7 +102,20 @@
 actionFinally :: Action a -> IO b -> Action a
 actionFinally = actionBoom True
 
+-- | If a syncronous exception is raised by the 'Action', perform some handler.
+--   Note that there is no guarantee that the handler will run on shutdown (use 'actionFinally' for that),
+--   and that 'actionCatch' /cannot/ catch exceptions thrown by dependencies, e.g. raised by 'need'
+--   (to do so would allow untracked dependencies on failure conditions).
+actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a
+actionCatch act hdl = Action $ catchRAW (fromAction act) $ \e ->
+    case () of
+        _ | not $ isAsyncException e
+          , Nothing <- fromException e :: Maybe ShakeException
+          , Just e <- fromException e
+          -> fromAction $ hdl e
+        _ -> throwRAW e
 
+
 -- | Retry an 'Action' if it throws an exception, at most /n/ times (where /n/ must be positive).
 --   If you need to call this function, you should probably try and fix the underlying cause (but you also probably know that).
 actionRetry :: Int -> Action a -> Action a
@@ -469,7 +482,7 @@
 --   If Shake ever has nothing else to do it will run batches before they are at the maximum,
 --   so you may see much smaller batches, especially at high parallelism settings.
 batch
-    :: Int   -- ^ Maximum number to run in a single batch, e.g. @3@.
+    :: Int   -- ^ Maximum number to run in a single batch, e.g. @3@, must be positive.
     -> ((a -> Action ()) -> Rules ()) -- ^ Way to match an entry, e.g. @\"*.ext\" '%>'@.
     -> (a -> Action b)  -- ^ Preparation to run individually on each, e.g. using 'need'.
     -> ([b] -> Action ())  -- ^ Combination action to run on all, e.g. using 'cmd'.
@@ -500,7 +513,7 @@
             -- delete at most mx from the batch
             (now, count) <- liftIO $ atomicModifyIORef todo $ \(count, bs) ->
                 let (now,later) = splitAt mx bs
-                    count2 = max 0 $ count - mx
+                    count2 = if count > mx then count - mx else 0
                 in ((count2, later), (now, count2))
             requeue todo (>=) count
 
diff --git a/src/Development/Shake/Internal/Core/Rules.hs b/src/Development/Shake/Internal/Core/Rules.hs
--- a/src/Development/Shake/Internal/Core/Rules.hs
+++ b/src/Development/Shake/Internal/Core/Rules.hs
@@ -183,7 +183,7 @@
 --
 --   Raises an error if any other rule exists at this type.
 addBuiltinRule
-    :: (RuleResult key ~ value, ShakeValue key, ShakeValue value, Partial)
+    :: (RuleResult key ~ value, ShakeValue key, Typeable value, NFData value, Show value, Partial)
     => BuiltinLint key value -> BuiltinIdentity key value -> BuiltinRun key value -> Rules ()
 addBuiltinRule = withFrozenCallStack $ addBuiltinRuleInternal $ BinaryOp
     (putEx . Bin.toLazyByteString . execPut . put)
diff --git a/src/Development/Shake/Internal/Rules/Rerun.hs b/src/Development/Shake/Internal/Rules/Rerun.hs
--- a/src/Development/Shake/Internal/Rules/Rerun.hs
+++ b/src/Development/Shake/Internal/Rules/Rerun.hs
@@ -31,7 +31,7 @@
 --     'Development.Shake.writeFileChanged' out stdout
 -- @
 --
---   In make, the @.PHONY@ attribute on file-producing rules has a similar effect.
+--   In @make@, the @.PHONY@ attribute on file-producing rules has a similar effect.
 --
 --   Note that 'alwaysRerun' is applied when a rule is executed. Modifying an existing rule
 --   to insert 'alwaysRerun' will /not/ cause that rule to rerun next time.
diff --git a/src/Test/Batch.hs b/src/Test/Batch.hs
--- a/src/Test/Batch.hs
+++ b/src/Test/Batch.hs
@@ -35,7 +35,10 @@
         o <- resultHasChanged out
         writeFileLines out $ xs ++ ["On" | o]
 
+    batch maxBound ("batch_max.*" %>) return $ \outs ->
+        forM_ outs $ \out -> writeFile' out $ show $ length outs
 
+
 test build = do
     forM_ [1..6] $ \i -> writeFile (show i <.> "in") $ show i
     build ["--sleep","-j2"]
@@ -86,3 +89,6 @@
         removeFile "On"
         build $ ["On", "--sleep"] ++ args
         assertContents "On" "On\n"
+
+    build ["batch_max." ++ show i | i <- [1..100]]
+    assertContents "batch_max.72" "100"
diff --git a/src/Test/Cache.hs b/src/Test/Cache.hs
--- a/src/Test/Cache.hs
+++ b/src/Test/Cache.hs
@@ -3,6 +3,7 @@
 
 import Development.Shake
 import Development.Shake.FilePath
+import System.Directory
 import Data.Char
 import Test.Type
 
@@ -15,7 +16,15 @@
     "*.out*" %> \x ->
         writeFile' x . show =<< vowels (dropExtension x <.> "txt")
 
+    startCompiler <- newCache $ \() -> do
+        liftIO $ writeFile "compiler.txt" "on"
+        runAfter $ writeFile "compiler.txt" "off"
 
+    "*.lang" %> \out -> do
+        startCompiler ()
+        liftIO $ copyFile "compiler.txt" out
+
+
 test build = do
     writeFile "trace.txt" ""
     writeFile "vowels.txt" "abc123a"
@@ -36,3 +45,10 @@
     build ["vowels.out1","-j3","--sleep"]
     assertContents "trace.txt" "111"
     assertContents "vowels.out1" "4"
+
+    build ["foo.lang","bar.lang"]
+    assertContents "foo.lang" "on"
+    assertContents "compiler.txt" "off"
+    writeFile "compiler.txt" "unstarted"
+    build ["foo.lang","bar.lang"]
+    assertContents "compiler.txt" "unstarted"
diff --git a/src/Test/Errors.hs b/src/Test/Errors.hs
--- a/src/Test/Errors.hs
+++ b/src/Test/Errors.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
+{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, DeriveDataTypeable, ScopedTypeVariables #-}
 
 module Test.Errors(main) where
 
@@ -169,6 +169,12 @@
             `actionFinally` (output "X" >> sleep 0.1)
             `actionFinally` output "Y"
 
+    let catching out = flip actionCatch $ \(e :: SomeException) -> writeFile' out $ show e
+    "catch1" %> \out -> catching out $ fail "magic1"
+    "catch2" %> \out -> catching out $ liftIO $ killThread =<< myThreadId
+    "catch3.1" %> \out -> fail "magic3"
+    "catch3.2" %> \out -> catching out $ need ["catch3.1"]
+
     -- not tested by default since only causes an error when idle GC is turned on
     phony "block" $
         liftIO $ putStrLn $ let x = x in x
@@ -286,3 +292,8 @@
     killThread t
     sleep 0.3
     assertContents "finalfinal" "XY"
+
+    build ["catch1"]
+    assertContents "catch1" "magic1"
+    crash ["catch2"] [show ThreadKilled]
+    crash ["catch3.2"] ["magic3"]
diff --git a/src/Test/Parallel.hs b/src/Test/Parallel.hs
--- a/src/Test/Parallel.hs
+++ b/src/Test/Parallel.hs
@@ -35,8 +35,13 @@
         peak <- liftIO $ readIORef peak
         writeFile' "parallel" $ show peak
 
+    "parallels" %> \out -> do
+        xs <- parallel $ replicate 5 $ parallel $ map return [1..5]
+        writeFile' out $ show xs
 
+
 test build = do
+    build ["clean"]
     writeFile "A.txt" "AAA"
     writeFile "B.txt" "BBB"
     build ["AB.txt","--sleep"]
@@ -51,3 +56,6 @@
     assertContents "parallel" "1"
     build ["parallel","-j5"]
     assertContents "parallel" "5"
+
+    build ["parallels"]
+    assertContents "parallels" $ show $ replicate 5 [1..5]
