diff --git a/shelly.cabal b/shelly.cabal
--- a/shelly.cabal
+++ b/shelly.cabal
@@ -1,6 +1,6 @@
 Name:       shelly
 
-Version:     1.5.3.2
+Version:     1.5.4
 Synopsis:    shell-like (systems) programming in Haskell
 
 Description: Shelly provides convenient systems programming in Haskell,
diff --git a/src/Shelly.hs b/src/Shelly.hs
--- a/src/Shelly.hs
+++ b/src/Shelly.hs
@@ -43,7 +43,7 @@
          , setenv, get_env, get_env_text, getenv, get_env_def, get_env_all, get_environment, appendToPath
 
          -- * Environment directory
-         , cd, chdir, pwd
+         , cd, chdir, chdir_p, pwd
 
          -- * Printing
          , echo, echo_n, echo_err, echo_n_err, inspect, inspect_err
@@ -446,10 +446,9 @@
 -- track of its own working directory and builds absolute paths internally
 -- instead of passing down relative paths.
 cd :: FilePath -> Sh ()
-cd = canonic >=> cd'
+cd = traceCanonicPath ("cd " <>) >=> cd'
   where
     cd' dir = do
-        trace $ "cd " <> tdir
         unlessM (test_d dir) $ errorExit $ "not a directory: " <> tdir
         modify $ \st -> st { sDirectory = dir, sPathExecutables = Nothing }
       where
@@ -485,9 +484,9 @@
 -- wraps system-fileio 'FileSystem.rename', which may not work across FS boundaries
 mv :: FilePath -> FilePath -> Sh ()
 mv from' to' = do
+  trace $ "mv " <> toTextIgnore from' <> " " <> toTextIgnore to'
   from <- absPath from'
   to <- absPath to'
-  trace $ "mv " <> toTextIgnore from <> " " <> toTextIgnore to
   to_dir <- test_d to
   let to_loc = if not to_dir then to else to FP.</> filename from
   liftIO $ rename from to_loc
@@ -525,16 +524,14 @@
 
 -- | Create a new directory (fails if the directory exists).
 mkdir :: FilePath -> Sh ()
-mkdir = absPath >=> \fp -> do
-  trace $ "mkdir " <> toTextIgnore fp
-  liftIO $ createDirectory False fp
+mkdir = traceAbsPath ("mkdir " <>) >=>
+        liftIO . createDirectory False
 
 -- | Create a new directory, including parents (succeeds if the directory
 -- already exists).
 mkdir_p :: FilePath -> Sh ()
-mkdir_p = absPath >=> \fp -> do
-  trace $ "mkdir -p " <> toTextIgnore fp
-  liftIO $ createTree fp
+mkdir_p = traceAbsPath ("mkdir -p " <>) >=>
+          liftIO . createTree
 
 -- | Create a new directory tree. You can describe a bunch of directories as
 -- a tree and this function will create all subdirectories. An example:
@@ -666,8 +663,8 @@
 -- own. Use carefully.
 -- Uses 'removeTree'
 rm_rf :: FilePath -> Sh ()
-rm_rf = absPath >=> \f -> do
-  trace $ "rm -rf " <> toTextIgnore f
+rm_rf infp = do
+  f <- traceAbsPath ("rm -rf " <>) infp
   isDir <- (test_d f)
   if not isDir then whenM (test_f f) $ rm_f f
     else
@@ -684,17 +681,15 @@
 -- | Remove a file. Does not fail if the file does not exist.
 -- Does fail if the file is not a file.
 rm_f :: FilePath -> Sh ()
-rm_f = absPath >=> \f -> do
-  trace $ "rm -f " <> toTextIgnore f
-  whenM (test_e f) $ canonic f >>= liftIO . removeFile
+rm_f = traceAbsPath ("rm -f " <>) >=> \f ->
+  whenM (test_e f) $ liftIO $ removeFile f
 
 -- | Remove a file.
 -- Does fail if the file does not exist (use 'rm_f' instead) or is not a file.
 rm :: FilePath -> Sh ()
-rm = absPath >=> \f -> do
-  trace $ "rm " <> toTextIgnore f
+rm = traceAbsPath ("rm " <>) >=>
   -- TODO: better error message for removeFile (give filename)
-  canonic f >>= liftIO . removeFile
+  liftIO . removeFile
 
 -- | Set an environment variable. The environment is maintained in Sh
 -- internally, and is passed to any external commands to be executed.
@@ -717,7 +712,7 @@
 
 -- | add the filepath onto the PATH env variable
 appendToPath :: FilePath -> Sh ()
-appendToPath = absPath >=> \filepath -> do
+appendToPath = traceAbsPath ("appendToPath: " <>) >=> \filepath -> do
   tp <- toTextWarn filepath
   pe <- get_env_text path_env
   setPath $ pe <> T.singleton searchPathSeparator <> tp
@@ -1179,8 +1174,8 @@
     from <- absPath from'
     fromIsDir <- (test_d from)
     if not fromIsDir then cp from' to' else do
+       trace $ "cp -r " <> toTextIgnore from <> " " <> toTextIgnore to'
        to <- absPath to'
-       trace $ "cp -r " <> toTextIgnore from <> " " <> toTextIgnore to
        toIsDir <- test_d to
 
        when (from == to) $ liftIO $ throwIO $ userError $ show $ "cp_r: " <>
@@ -1216,7 +1211,7 @@
   trace "withTmpDir"
   dir <- liftIO getTemporaryDirectory
   tid <- liftIO myThreadId
-  (pS, handle) <- liftIO $ openTempFile dir ("tmp"++filter isAlphaNum (show tid))
+  (pS, handle) <- liftIO $ openTempFile dir ("tmp" ++ filter isAlphaNum (show tid))
   let p = pack pS
   liftIO $ hClose handle -- required on windows
   rm_f p
@@ -1225,29 +1220,29 @@
 
 -- | Write a Lazy Text to a file.
 writefile :: FilePath -> Text -> Sh ()
-writefile f' bits = absPath f' >>= \f -> do
-  trace $ "writefile " <> toTextIgnore f
+writefile f' bits = do
+  f <- traceAbsPath ("writefile " <>) f'
   liftIO (TIO.writeFile (encodeString f) bits)
 
 -- | Update a file, creating (a blank file) if it does not exist.
 touchfile :: FilePath -> Sh ()
-touchfile = absPath >=> flip appendfile ""
+touchfile = traceAbsPath ("touch " <>) >=> flip appendfile ""
 
 -- | Append a Lazy Text to a file.
 appendfile :: FilePath -> Text -> Sh ()
-appendfile f' bits = absPath f' >>= \f -> do
-  trace $ "appendfile " <> toTextIgnore f
+appendfile f' bits = do
+  f <- traceAbsPath ("appendfile " <>) f'
   liftIO (TIO.appendFile (encodeString f) bits)
 
 readfile :: FilePath -> Sh Text
-readfile = absPath >=> \fp -> do
-  trace $ "readfile " <> toTextIgnore fp
+readfile = traceAbsPath ("readfile " <>) >=> \fp ->
   readBinary fp >>=
     return . TE.decodeUtf8With TE.lenientDecode
 
 -- | wraps ByteSting readFile
 readBinary :: FilePath -> Sh ByteString
-readBinary = absPath >=> liftIO . BS.readFile . encodeString
+readBinary = traceAbsPath ("readBinary " <>)
+         >=> liftIO . BS.readFile . encodeString
 
 -- | flipped hasExtension for Text
 hasExt :: Text -> FilePath -> Bool
@@ -1271,3 +1266,20 @@
 asyncSh proc = do
   state <- get
   liftIO $ async $ shelly (put state >> proc)
+
+-- helper because absPath can throw exceptions
+-- This helps give clear tracing messages
+tracePath :: (FilePath -> Sh FilePath) -- ^ filepath conversion
+          -> (Text -> Text) -- ^ tracing statement
+          -> FilePath
+          -> Sh FilePath -- ^ converted filepath
+tracePath convert tracer infp =
+  (convert infp >>= \fp -> traceIt fp >> return fp)
+  `catchany_sh` (\e -> traceIt infp >> liftIO (throwIO e))
+    where traceIt = trace . tracer . toTextIgnore
+
+traceAbsPath :: (Text -> Text) -> FilePath -> Sh FilePath
+traceAbsPath = tracePath absPath
+
+traceCanonicPath :: (Text -> Text) -> FilePath -> Sh FilePath
+traceCanonicPath = tracePath canonic
diff --git a/src/Shelly/Base.hs b/src/Shelly/Base.hs
--- a/src/Shelly/Base.hs
+++ b/src/Shelly/Base.hs
@@ -6,6 +6,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE DeriveDataTypeable #-}
 -- | I started exposing multiple module (starting with one for finding)
 -- Base prevented circular dependencies
 -- However, Shelly went back to exposing a single module
@@ -49,13 +50,14 @@
 import Data.Monoid (mappend)
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
-import Control.Exception (SomeException, catch)
+import Control.Exception (SomeException, catch, throwIO, Exception)
 import Data.Maybe (fromMaybe)
 import qualified Control.Monad.Catch as Catch
 import Control.Monad.Trans ( MonadIO, liftIO )
 import Control.Monad.Reader.Class (MonadReader, ask)
 import Control.Monad.Trans.Reader (runReaderT, ReaderT(..))
 import qualified Data.Set as S
+import Data.Typeable (Typeable)
 
 -- | ShIO is Deprecated in favor of 'Sh', which is easier to type.
 type ShIO a = Sh a
@@ -192,11 +194,17 @@
    if not was_dir then FS.canonicalizePath p
      else addTrailingSlash `fmap` FS.canonicalizePath p
 
+data EmptyFilePathError = EmptyFilePathError deriving Typeable
+instance Show EmptyFilePathError where
+    show _ = "Empty filepath"
+instance Exception EmptyFilePathError
+
 -- | Make a relative path absolute by combining with the working directory.
 -- An absolute path is returned as is.
 -- To create a relative path, use 'relPath'.
 absPath :: FilePath -> Sh FilePath
-absPath p | relative p = (FP.</> p) <$> gets sDirectory
+absPath p | FP.null p = liftIO $ throwIO EmptyFilePathError
+          | relative p = (FP.</> p) <$> gets sDirectory
           | otherwise = return p
 
 -- | deprecated
diff --git a/src/Shelly/Lifted.hs b/src/Shelly/Lifted.hs
--- a/src/Shelly/Lifted.hs
+++ b/src/Shelly/Lifted.hs
@@ -52,7 +52,7 @@
          , setenv, get_env, get_env_text, get_env_all, appendToPath
 
          -- * Environment directory
-         , cd, chdir, pwd
+         , cd, chdir, chdir_p, pwd
 
          -- * Printing
          , echo, echo_n, echo_err, echo_n_err, inspect, inspect_err
@@ -303,6 +303,9 @@
 
 chdir :: MonadShControl m => FilePath -> m a -> m a
 chdir dir action = controlSh $ \runInSh -> S.chdir dir (runInSh action)
+
+chdir_p :: MonadShControl m => FilePath -> m a -> m a
+chdir_p dir action = controlSh $ \runInSh -> S.chdir_p dir (runInSh action)
 
 silently :: MonadShControl m => m a -> m a
 silently a = controlSh $ \runInSh -> S.silently (runInSh a)
