diff --git a/src/Yesod/Auth/OAuth2/Dispatch.hs b/src/Yesod/Auth/OAuth2/Dispatch.hs
--- a/src/Yesod/Auth/OAuth2/Dispatch.hs
+++ b/src/Yesod/Auth/OAuth2/Dispatch.hs
@@ -9,7 +9,7 @@
     ) where
 
 import Control.Exception.Safe (throwString, tryIO)
-import Control.Monad (unless)
+import Control.Monad (unless, (<=<))
 import Data.Monoid ((<>))
 import Data.Text (Text)
 import qualified Data.Text as T
@@ -19,6 +19,7 @@
 import System.Random (newStdGen, randomRs)
 import URI.ByteString.Extension
 import Yesod.Auth
+import Yesod.Auth.OAuth2.ErrorResponse (onErrorResponse)
 import Yesod.Core
 
 -- | How to take an @'OAuth2Token'@ and retrieve user credentials
@@ -56,6 +57,7 @@
 dispatchCallback :: Text -> OAuth2 -> FetchCreds m -> AuthHandler m TypedContent
 dispatchCallback name oauth2 getCreds = do
     csrf <- verifySessionCSRF $ tokenSessionKey name
+    onErrorResponse errInvalidOAuth
     code <- requireGetParam "code"
     manager <- lift $ getsYesod authHttpManager
     oauth2' <- withCallbackAndState name oauth2 csrf
@@ -65,15 +67,12 @@
   where
     -- On a Left result, log it and return an opaque permission-denied
     denyLeft :: (MonadHandler m, MonadLogger m, Show e) => IO (Either e a) -> m a
-    denyLeft act = do
-        result <- liftIO act
-        either
-            (\err -> do
-                $(logError) $ T.pack $ "OAuth2 error: " <> show err
-                permissionDenied "Invalid OAuth2 authentication attempt"
-            )
-            return
-            result
+    denyLeft = either errInvalidOAuth pure <=< liftIO
+
+    errInvalidOAuth :: (MonadHandler m, MonadLogger m, Show e) => e -> m a
+    errInvalidOAuth err = do
+        $(logError) $ T.pack $ "OAuth2 error: " <> show err
+        permissionDenied "Invalid OAuth2 authentication attempt"
 
 withCallbackAndState :: Text -> OAuth2 -> Text -> AuthHandler m OAuth2
 withCallbackAndState name oauth2 csrf = do
diff --git a/src/Yesod/Auth/OAuth2/ErrorResponse.hs b/src/Yesod/Auth/OAuth2/ErrorResponse.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/Auth/OAuth2/ErrorResponse.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | OAuth callback error response
+--
+-- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>
+--
+module Yesod.Auth.OAuth2.ErrorResponse
+    ( ErrorResponse(..)
+    , ErrorName(..)
+    , onErrorResponse
+    ) where
+
+import Data.Foldable (traverse_)
+import Data.Text (Text)
+import Data.Traversable (for)
+import Yesod.Core (MonadHandler, lookupGetParam)
+
+data ErrorName
+    = InvalidRequest
+    | UnauthorizedClient
+    | AccessDenied
+    | UnsupportedResponseType
+    | InvalidScope
+    | ServerError
+    | TemporarilyUnavailable
+    | Unknown Text
+    deriving Show
+
+data ErrorResponse = ErrorResponse
+    { erName :: ErrorName
+    , erDescription :: Maybe Text
+    , erURI :: Maybe Text
+    }
+    deriving Show
+
+-- | Check query parameters for an error, if found run the given action
+--
+-- The action is expected to use a short-circuit response function like
+-- @'permissionDenied'@, hence this returning @()@.
+--
+onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m ()
+onErrorResponse f = traverse_ f =<< checkErrorResponse
+
+checkErrorResponse :: MonadHandler m => m (Maybe ErrorResponse)
+checkErrorResponse = do
+    merror <- lookupGetParam "error"
+
+    for merror $ \err -> ErrorResponse
+        <$> pure (readErrorName err)
+        <*> lookupGetParam "error_description"
+        <*> lookupGetParam "error_uri"
+
+readErrorName :: Text -> ErrorName
+readErrorName "invalid_request" = InvalidRequest
+readErrorName "unauthorized_client" = UnauthorizedClient
+readErrorName "access_denied" = AccessDenied
+readErrorName "unsupported_response_type" = UnsupportedResponseType
+readErrorName "invalid_scope" = InvalidScope
+readErrorName "server_error" = ServerError
+readErrorName "temporarily_unavailable" = TemporarilyUnavailable
+readErrorName x = Unknown x
diff --git a/yesod-auth-oauth2.cabal b/yesod-auth-oauth2.cabal
--- a/yesod-auth-oauth2.cabal
+++ b/yesod-auth-oauth2.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: d175e5c0f1cc9ab1436a5288b11c394c23922e9bb4e9cf2cbf49033dcfd9f84f
+-- hash: 2abd54f1d50a9de6207536db36d9e0d29146b6f7e9f8b6495dc9f4377f83bc84
 
 name:           yesod-auth-oauth2
-version:        0.4.0.1
+version:        0.4.1.0
 synopsis:       OAuth 2.0 authentication plugins
 description:    Library to authenticate with OAuth 2.0 for Yesod web applications.
 category:       Web
@@ -54,6 +54,7 @@
       Yesod.Auth.OAuth2.BattleNet
       Yesod.Auth.OAuth2.Bitbucket
       Yesod.Auth.OAuth2.Dispatch
+      Yesod.Auth.OAuth2.ErrorResponse
       Yesod.Auth.OAuth2.EveOnline
       Yesod.Auth.OAuth2.Github
       Yesod.Auth.OAuth2.Google
