diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@
 and this project adheres to the
 [Haskell Package Versioning Policy](https://pvp.haskell.org/).
 
+## 0.4.0.0 - 2025-04-02
+
+* On Unix-like operating systems, use Haskell to silence the opening
+  application or script, rather than the `sh` shell on Linux and BSD.
+* Add utility `openBrowserWithExitCode`, to help with debugging failure.
+* Improve Haddock documentation.
+
 ## 0.3.0.1 - 2025-03-17
 
 * On Windows, remove unnecessary dependency on `process`.
@@ -31,8 +38,9 @@
 
 * As released by rightfold on Hackage. This entry and prior change log is
   reconstructed.
-* Support GHC versions before GHC 8.0.
-* Add support for 32-bit Windows.
+* Support GHC versions before GHC 8.0, by removing the Byte Order Mark from
+  source files.
+* Add support for 32-bit Windows, by using the `stdcall` calling convention.
 
 ## 0.2.0.0 - 2015-07-31
 
@@ -40,11 +48,13 @@
 
 ## 0.1.4.0 - 2015-07-30
 
-* On Linux, silence `xdg-open`.
+* On Linux and BSD, silence the `xdg-open` script using the `sh` shell.
 
 ## 0.1.3.0 - 2015-07-27
 
-* Add support for BSD and Windows.
+* Add support for BSD, using the `xdg-open` script.
+* Add support for Windows, using the Win32 API and the `ccall` calling
+  convention.
 
 ## 0.1.2.0 - 2015-07-27
 
@@ -52,8 +62,9 @@
 
 ## 0.1.1.0 - 2015-07-27
 
-* Add support for Linux.
+* Add support for Linux, using the `xdg-open` script.
+* On OS X, use the `open` application.
 
 ## 0.1.0.0 - 2015-07-25
 
-* Initial version. Only OS X supported.
+* Initial version. Only OS X supported, using `open location` in an AppleScript.
diff --git a/example/Main.hs b/example/Main.hs
--- a/example/Main.hs
+++ b/example/Main.hs
@@ -4,9 +4,31 @@
   ( main
   ) where
 
-import Web.Browser ( openBrowser )
+import Control.Exception ( Exception (..), SomeException )
+import System.Exit ( ExitCode (..) )
+import Web.Browser ( openBrowser, openBrowserWithExitCode )
 
 main :: IO ()
-main = openBrowser "https://haskell.org/" >>= \case
-  True -> putStrLn "The operation succeeded!"
-  False -> putStrLn "The operation failed!"
+main = do
+  openBrowser "https://haskell.org/" >>= \case
+    True -> putStrLn "The operation succeeded!"
+    False -> putStrLn "The operation failed!"
+  blankLine
+  putStrLn "Help with debugging:"
+  putStrLn "Try a good URL:"
+  tryOpenGoodUrl >>= reportResult
+  blankLine
+  putStrLn "Try a bad URL:"
+  tryOpenBadUrl >>= reportResult
+ where
+  tryOpenGoodUrl :: IO (Either SomeException (ExitCode, String, String))
+  tryOpenGoodUrl = openBrowserWithExitCode "https://haskell.org/"
+  tryOpenBadUrl :: IO (Either SomeException (ExitCode, String, String))
+  tryOpenBadUrl = openBrowserWithExitCode "example-bad-url"
+  reportResult = \case
+    Left e -> putStrLn $ "Exception: " <> displayException e
+    Right (ec, out, err) -> do
+      putStrLn $ "Exit code: " <> show ec
+      putStrLn $ "Standard output: " <> out
+      putStrLn $ "Standard error: " <> err
+  blankLine = putStrLn ""
diff --git a/lib/Web/Browser.hs b/lib/Web/Browser.hs
--- a/lib/Web/Browser.hs
+++ b/lib/Web/Browser.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE LambdaCase #-}
+
 {-|
 Module      : Web.Browser
 Description : Open a web browser from Haskell
@@ -11,14 +13,18 @@
 
 module Web.Browser
   ( openBrowser
+    -- * Utilities
+  , openBrowserWithExitCode
   ) where
 
+import           Control.Exception ( Exception (..), SomeException, try)
+import           System.Exit ( ExitCode (..) )
 import qualified Web.Browser.OS as OS
 
--- | Seeks to open the given item. If the item is a URL or another item
--- associated with a web browser (for example, it represents a local @.html@
--- file), seeks to open it in the user's preferred web browser. Returns whether
--- or not the operation succeeded.
+-- | Seeks to open the given item, silently. If the item is a URL or another
+-- item associated with a web browser (for example, it represents a local
+-- @.html@ file), seeks to open it in the user's preferred web browser. Returns
+-- whether or not the operation succeeded.
 --
 -- No checks are performed on the nature or validity of the given item.
 --
@@ -31,13 +37,33 @@
 --   item that represents a file, equivalent to double-clicking on the file's
 --   icon; and
 --
--- * on Linux, FreeBSD, OpenBSD or NetBSD, the \'xdg-open\' application, if it
---   is on the user's PATH, via \'sh\' to allow the application's output to be
---   silenced.
+-- * on Linux, FreeBSD, OpenBSD or NetBSD, the \'xdg-open\' script, if it
+--   is on the user's PATH.
 --
 -- On other operating systems, the operation always fails.
+--
+-- @since 0.1.0.0
 openBrowser ::
      String
-     -- ^ URL or other item to try to open
+     -- ^ URL or other item to try to open.
   -> IO Bool
-openBrowser = OS.openBrowser
+openBrowser url = tryOpenUrl >>= \case
+  Left _ -> pure False
+  Right (ec, _, _) -> pure $ ec == ExitSuccess
+ where
+  tryOpenUrl :: IO (Either SomeException (ExitCode, String, String))
+  tryOpenUrl = openBrowserWithExitCode url
+
+-- | Exported to help with debugging. As for 'openBrowser' but returns either an
+-- exception or, as a triple, the 'ExitCode' of the opening mechanism and any
+-- output to the standard output and standard error channels. On failure, the
+-- meaning of the exit code will depend on the operating system; for unsupported
+-- operating systems, it will be 'ExitFailure' @1@.
+--
+-- @since 0.4.0.0
+openBrowserWithExitCode ::
+     Exception e
+  => String
+     -- ^ URL or other item to try to open.
+  -> IO (Either e (ExitCode, String, String))
+openBrowserWithExitCode url = try $ OS.openBrowserWithExitCode url
diff --git a/lib/unix-like/common/Utils.hs b/lib/unix-like/common/Utils.hs
deleted file mode 100644
--- a/lib/unix-like/common/Utils.hs
+++ /dev/null
@@ -1,20 +0,0 @@
---------------------------------------------------------------------------------
--- For operating systems that use an application on the PATH to open the URL.
---------------------------------------------------------------------------------
-
-module Utils
-  ( openBrowserWith
-  ) where
-
-import System.Exit ( ExitCode (..) )
-import System.Process ( proc, waitForProcess, withCreateProcess )
-
-openBrowserWith ::
-     String
-     -- ^ Name of relevant executable on the PATH.
-  -> [String]
-     -- ^ Arguments for executable.
-  -> IO Bool
-openBrowserWith cmd args =
-  withCreateProcess (proc cmd args) $ \_ _ _ p ->
-    (== ExitSuccess) <$> waitForProcess p
diff --git a/lib/unix-like/open/Web/Browser/OS.hs b/lib/unix-like/open/Web/Browser/OS.hs
--- a/lib/unix-like/open/Web/Browser/OS.hs
+++ b/lib/unix-like/open/Web/Browser/OS.hs
@@ -4,14 +4,15 @@
 --------------------------------------------------------------------------------
 
 module Web.Browser.OS
-  ( openBrowser
+  ( openBrowserWithExitCode
   ) where
 
-import Utils ( openBrowserWith )
+import System.Exit ( ExitCode (..) )
+import System.Process ( readProcessWithExitCode )
 
 -- https://ss64.com/mac/open.html
-openBrowser ::
+openBrowserWithExitCode ::
      String
-     -- ^ URL
-  -> IO Bool
-openBrowser url = openBrowserWith "open" [url]
+     -- ^ URL or other item to try to open.
+  -> IO (ExitCode, String, String)
+openBrowserWithExitCode url = readProcessWithExitCode "open" [url] ""
diff --git a/lib/unix-like/xdg-open/Web/Browser/OS.hs b/lib/unix-like/xdg-open/Web/Browser/OS.hs
--- a/lib/unix-like/xdg-open/Web/Browser/OS.hs
+++ b/lib/unix-like/xdg-open/Web/Browser/OS.hs
@@ -1,31 +1,17 @@
 --------------------------------------------------------------------------------
--- For Unix-like operating systems that provide the xdg-open application on the
--- PATH.
+-- For Unix-like operating systems that provide the xdg-open script on the PATH.
 --------------------------------------------------------------------------------
 
 module Web.Browser.OS
-  ( openBrowser
+  ( openBrowserWithExitCode
   ) where
 
-import Utils ( openBrowserWith )
+import System.Exit ( ExitCode (..) )
+import System.Process ( readProcessWithExitCode )
 
 -- https://ss64.com/bash/xdg-open.html
-openBrowser ::
+openBrowserWithExitCode ::
      String
-     -- ^ URL
-  -> IO Bool
-openBrowser url =
-  -- sh -c '...'
-  --
-  -- tells the shell (sh) to execute the given string as a command, and
-  -- the following argument (url) is passed as a positional argument $0.
-  --
-  -- xdg-open "$0" 2>&1 > /dev/null
-  --
-  -- The $0 is expanded and the double quotes ensures it is treated as a single
-  -- argument to xdg-open.
-  --
-  -- 2>&1 redirects standard error to standard output.
-  --
-  -- > /dev/null discards standard output.
-  openBrowserWith "sh" ["-c", "xdg-open \"$0\" 2>&1 > /dev/null", url]
+     -- ^ URL or other item to try to open.
+  -> IO (ExitCode, String, String)
+openBrowserWithExitCode url = readProcessWithExitCode "xdg-open" [url] ""
diff --git a/lib/unsupported-os/Web/Browser/OS.hs b/lib/unsupported-os/Web/Browser/OS.hs
--- a/lib/unsupported-os/Web/Browser/OS.hs
+++ b/lib/unsupported-os/Web/Browser/OS.hs
@@ -3,11 +3,15 @@
 --------------------------------------------------------------------------------
 
 module Web.Browser.OS
-  ( openBrowser
+  ( openBrowserWithExitCode
   ) where
 
-openBrowser ::
+import System.Exit ( ExitCode (..) )
+
+openBrowserWithExitCode ::
      String
-     -- ^ URL
-  -> IO Bool
-openBrowser = const (pure False) -- Operation never succeeds
+     -- ^ URL or other item to try to open.
+  -> IO (ExitCode, String, String)
+openBrowserWithExitCode =
+  -- Operation never succeeds
+  const (pure (ExitFailure 1, "", "Unsupported operating system."))
diff --git a/lib/windows/Web/Browser/OS.hs b/lib/windows/Web/Browser/OS.hs
--- a/lib/windows/Web/Browser/OS.hs
+++ b/lib/windows/Web/Browser/OS.hs
@@ -8,9 +8,10 @@
 --------------------------------------------------------------------------------
 
 module Web.Browser.OS
-  ( openBrowser
+  ( openBrowserWithExitCode
   ) where
 
+import System.Exit ( ExitCode (..) )
 import System.Win32.Types
          ( HANDLE, HINSTANCE, INT, LPCWSTR, handleToWord, nullPtr, withTString )
 
@@ -24,16 +25,18 @@
 sW_SHOWNORMAL :: INT
 sW_SHOWNORMAL = 1
 
-openBrowser ::
+openBrowserWithExitCode ::
      String
-     -- ^ URL
-  -> IO Bool
-openBrowser url =
+     -- ^ URL or other item to try to open.
+  -> IO (ExitCode, String, String)
+openBrowserWithExitCode url =
   withTString "open" $ \openStr ->
-    withTString url $ \urlStr -> exitCodeToBool <$>
+    withTString url $ \urlStr -> exitcodeToExitCode <$>
       c_ShellExecute nullPtr openStr urlStr nullPtr nullPtr sW_SHOWNORMAL
  where
-  exitCodeToBool hinst = handleToWord hinst > 32
+  exitcodeToExitCode hinst =
+    let exitcode = fromIntegral $ handleToWord hinst
+    in  (if exitcode > 32 then ExitSuccess else ExitFailure exitcode, "", "")
 
 -- https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew
 foreign import ccall unsafe "windows.h ShellExecuteW"
diff --git a/open-browser.cabal b/open-browser.cabal
--- a/open-browser.cabal
+++ b/open-browser.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           open-browser
-version:        0.3.0.1
+version:        0.4.0.0
 synopsis:       Open a web browser from Haskell
 description:    Open a web browser from Haskell. Windows, macOS, Linux and BSD are supported.
 category:       Web
@@ -51,23 +51,19 @@
   else
     if os(darwin)
       other-modules:
-          Utils
           Web.Browser.OS
       hs-source-dirs:
-          lib/unix-like/common
           lib/unix-like/open
       build-depends:
-          process >=1.4.3.0 && <2
+          process >=1.2.0.0 && <2
     else
       if os(linux) || os(freebsd) || os(openbsd) || os(netbsd)
         other-modules:
-            Utils
             Web.Browser.OS
         hs-source-dirs:
-            lib/unix-like/common
             lib/unix-like/xdg-open
         build-depends:
-            process >=1.4.3.0 && <2
+            process >=1.2.0.0 && <2
       else
         other-modules:
             Web.Browser.OS
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,1 +1,1 @@
-snapshot: lts-23.14 # GHC 9.8.4
+snapshot: lts-23.17 # GHC 9.8.4
diff --git a/stack.yaml.lock b/stack.yaml.lock
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -1,12 +1,12 @@
-# This file was autogenerated by Stack.
-# You should not edit this file by hand.
-# For more information, please see the documentation at:
-#   https://docs.haskellstack.org/en/stable/topics/lock_files
-
-packages: []
-snapshots:
-- completed:
-    sha256: 1964d439d2a152be4238053f3f997a09fb348391984daab86d724975ef9a423f
-    size: 683814
-    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/14.yaml
-  original: lts-23.14
+# This file was autogenerated by Stack.
+# You should not edit this file by hand.
+# For more information, please see the documentation at:
+#   https://docs.haskellstack.org/en/stable/topics/lock_files
+
+packages: []
+snapshots:
+- completed:
+    sha256: 2763632e4c4094ce12f5ae12b22f524cdc6453b6b19007ff164a37fd9d2ea829
+    size: 683819
+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/17.yaml
+  original: lts-23.17
