diff --git a/flag.api.php b/flag.api.php
index 4c4e962..96201ab 100644
--- a/flag.api.php
+++ b/flag.api.php
@@ -121,6 +121,36 @@ function hook_flag_unflag($flag, $entity_id, $account, $flagging) {
 }
 
 /**
+ * Perform custom validation on a flag before flagging/unflagging.
+ *
+ * @param $action
+ *  The action to test. Either 'flag' or 'unflag'.
+ * @param $flag
+ *  The flag object.
+ * @param $entity_id
+ *  The id of the entity the flag is on.
+ * @param $account
+ *  The user account performing the action.
+ * @param $flagging
+ *  The flagging entity.
+ */
+function hook_flag_validate($action, $flag, $entity_id, $account, $skip_permission_check, $flagging) {
+  // We're only operating on the "test" flag, and users may always unflag.
+  if ($flag->name == 'test' && $action == 'flag') {
+    // Get all flags set by the current user.
+    $flags = flag_get_user_flags('node', NULL, $account->uid, $sid = NULL, $reset = FALSE);
+    // Check if this user has any flags of this type set.
+    if (isset($flags['test'])) {
+      $count = count($flags[$flag->name]);
+      if ($count >= 2) {
+        // Users may flag only 2 nodes with this flag.
+        return(array('access-denied' => t('You may only flag 2 nodes with the test flag.')));
+      }
+    }
+  }
+}
+
+/**
  * Allow modules to allow or deny access to flagging.
  *
  * @param $flag
diff --git a/flag.module b/flag.module
index e5cee50..89f6cb7 100644
--- a/flag.module
+++ b/flag.module
@@ -1579,7 +1579,7 @@ function flag_theme() {
 
   return array(
     'flag' => array(
-      'variables' => array('flag' => NULL, 'action' => NULL, 'entity_id' => NULL, 'after_flagging' => FALSE),
+      'variables' => array('flag' => NULL, 'action' => NULL, 'entity_id' => NULL, 'after_flagging' => FALSE, 'errors' => array()),
       'template' => 'flag',
       'pattern' => 'flag__',
       'path' => $path,
@@ -1616,6 +1616,7 @@ function flag_theme() {
  * - $action
  * - $entity_id
  * - $after_flagging
+ * - $errors
  *
  * See 'flag.tpl.php' for their documentation.
  */
@@ -1627,6 +1628,7 @@ function template_preprocess_flag(&$variables) {
   $flag =& $variables['flag'];
   $action = $variables['action'];
   $entity_id = $variables['entity_id'];
+  $errors = join('<br />', $variables['errors']);
   $flag_css_name = str_replace('_', '-', $flag->name);
 
   // Generate the link URL.
@@ -1669,7 +1671,6 @@ function template_preprocess_flag(&$variables) {
   $variables['flag_wrapper_classes_array'][] = 'flag-wrapper';
   $variables['flag_wrapper_classes_array'][] = 'flag-' . $flag_css_name;
   $variables['flag_wrapper_classes_array'][] = 'flag-' . $flag_css_name . '-' . $entity_id;
-  $variables['flag_wrapper_classes'] = implode(' ', $variables['flag_wrapper_classes_array']);
 
   $variables['flag_classes_array'] = array();
   $variables['flag_classes_array'][] = 'flag';
@@ -1684,12 +1685,30 @@ function template_preprocess_flag(&$variables) {
     $link['attributes']['class'] = is_string($link['attributes']['class']) ? array_filter(explode(' ', $link['attributes']['class'])) : $link['attributes']['class'];
     $variables['flag_classes_array'] = array_merge($variables['flag_classes_array'], $link['attributes']['class']);
   }
+  $variables['message_classes_array'] = array();
   if ($variables['after_flagging']) {
-    $inverse_action = ($action == 'flag' ? 'unflag' : 'flag');
-    $variables['message_text'] = $flag->get_label($inverse_action . '_message', $entity_id);
-    $variables['flag_classes_array'][] = $variables['status'];
+    $variables['message_classes_array'][] = 'flag-message';
+    if ($errors) {
+      $variables['message_classes_array'][] = 'flag-failure-message';
+      $variables['message_text'] = $errors;
+    }
+    else {
+      $inverse_action = ($action == 'flag' ? 'unflag' : 'flag');
+      $variables['message_classes_array'][] = 'flag-success-message';
+      $variables['message_classes_array'][] = 'flag-' . $variables['status'] . '-message';
+      $variables['message_text'] = $flag->get_label($inverse_action . '_message', $entity_id);
+      $variables['flag_classes_array'][] = $variables['status'];
+      // By default we make our JS code remove, after a few seconds, only success messages.
+      $variables['message_classes_array'][] = 'flag-auto-remove';
+    }
   }
+  else {
+    $variables['message_text'] = '';
+  }
+
+  $variables['flag_wrapper_classes'] = implode(' ', $variables['flag_wrapper_classes_array']);
   $variables['flag_classes'] = implode(' ', $variables['flag_classes_array']);
+  $variables['message_classes'] = join(' ', $variables['message_classes_array']);
 
   // Backward compatibility: The following section preserves some deprecated
   // variables either to make old flag.tpl.php files continue to work, or to
diff --git a/includes/flag.pages.inc b/includes/flag.pages.inc
index d7334e0..944acd7 100644
--- a/includes/flag.pages.inc
+++ b/includes/flag.pages.inc
@@ -25,45 +25,40 @@ function flag_page($action, $flag, $entity_id) {
 
   // Check the flag token, then perform the flagging.
   if (!flag_check_token($token, $entity_id)) {
-    $error = t('Bad token. You seem to have followed an invalid link.');
+    $flag->errors['token'] = t('Bad token. You seem to have followed an invalid link.');
   }
   elseif ($user->uid == 0 && !$has_js) {
-    $error = t('You must have JavaScript and cookies enabled in your browser to flag content.');
+    $flag->errors['javascript'] = t('You must have JavaScript and cookies enabled in your browser to flag content.');
   }
   else {
-    $result = $flag->flag($action, $entity_id);
-    if (!$result) {
-      $error = t('You are not allowed to flag, or unflag, this content.');
-    }
-  }
-
-  // If an error was received, set a message and exit.
-  if (isset($error)) {
-    if ($js) {
-      drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8');
-      print drupal_json_encode(array(
-        'status' => FALSE,
-        'errorMessage' => $error,
-      ));
-      drupal_exit();
-    }
-    else {
-      drupal_set_message($error);
-      drupal_access_denied();
-      return;
-    }
+    $flag->flag($action, $entity_id);
   }
 
   // If successful, return data according to the request type.
   if ($js) {
     drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8');
     $flag->link_type = 'toggle';
+    // Any errors that have been set will be output below the flag link with javascript.
     print drupal_json_encode(flag_build_javascript_info($flag, $entity_id));
     drupal_exit();
   }
   else {
-    drupal_set_message($flag->get_label($action . '_message', $entity_id));
-    drupal_goto();
+    if ($errors = $flag->get_errors()) {
+      // If an error was received, set a message and exit.
+      foreach ($errors as $error) {
+        drupal_set_message($error, 'error');
+      }
+      if (isset($errors['access-denied'])) {
+        drupal_access_denied();
+      }
+      else {
+        drupal_goto();
+      }
+    }
+    else {
+      drupal_set_message($flag->get_label($action . '_message', $entity_id));
+      drupal_goto();
+    }
   }
 }
 
@@ -135,7 +130,11 @@ function flag_confirm_submit(&$form, &$form_state) {
   }
 
   if (!$result) {
-    drupal_set_message(t('You are not allowed to flag, or unflag, this content.'));
+    if ($errors = $flag->get_errors()) {
+      foreach ($errors as $error) {
+        drupal_set_message($error, 'error');
+      }
+    }
   }
   else {
     drupal_set_message($flag->get_label($action . '_message', $entity_id));
@@ -146,10 +145,12 @@ function flag_confirm_submit(&$form, &$form_state) {
  * Builds the JavaScript structure describing the flagging operation.
  */
 function flag_build_javascript_info($flag, $entity_id) {
+  $errors = $flag->get_errors();
   $info = array(
     'status' => TRUE,
-    'newLink' => $flag->theme($flag->is_flagged($entity_id) ? 'unflag' : 'flag', $entity_id, TRUE),
+    'newLink' => $flag->theme($flag->is_flagged($entity_id) ? 'unflag' : 'flag', $entity_id, TRUE, $errors),
     // Further information for the benefit of custom JavaScript event handlers:
+    'flagSuccess' => !$errors,
     'contentId' => $entity_id,
     'entityType' => $flag->entity_type,
     'flagName' => $flag->name,
diff --git a/includes/flag/flag_flag.inc b/includes/flag/flag_flag.inc
index d69a008..c77e61a 100644
--- a/includes/flag/flag_flag.inc
+++ b/includes/flag/flag_flag.inc
@@ -94,6 +94,18 @@ class flag_flag {
   );
 
   /**
+   * An associative array containing textual errors that may be created during validation.
+   *
+   * The array keys should reflect the type of error being set. At this time,
+   * the only "special" behavior related to the array keys is that
+   * drupal_access_denied() is called when the key is 'access-denied' and
+   * javascript is disabled.
+   *
+   * @var array
+   */
+  var $errors = array();
+
+  /**
    * Creates a flag from a database row. Returns it.
    *
    * This is static method.
@@ -641,7 +653,7 @@ class flag_flag {
    *   applicable to the item, etc.), TRUE otherwise.
    */
   function flag($action, $entity_id, $account = NULL, $skip_permission_check = FALSE, $flagging = NULL) {
-     // Get the user.
+    // Get the user.
     if (!isset($account)) {
       $account = $GLOBALS['user'];
     }
@@ -652,6 +664,7 @@ class flag_flag {
     // Check access and applicability.
     if (!$skip_permission_check) {
       if (!$this->access($entity_id, $action, $account)) {
+        $this->errors['access-denied'] = t('You are not allowed to flag, or unflag, this content.');
         // User has no permission to flag/unflag this object.
         return FALSE;
       }
@@ -660,10 +673,15 @@ class flag_flag {
       // We are skipping permission checks. However, at a minimum we must make
       // sure the flag applies to this entity type:
       if (!$this->applies_to_entity_id($entity_id)) {
+        $this->errors['entity-type'] = t('This flag does not apply to this entity type.');
         return FALSE;
       }
     }
 
+    if (($this->errors = module_invoke_all('flag_validate', $action, $this, $entity_id, $account, $skip_permission_check, $flagging))) {
+      return FALSE;
+    }
+
     // Clear various caches; We don't want code running after us to report
     // wrong counts or false flaggings.
     flag_get_counts(NULL, NULL, TRUE);
@@ -681,6 +699,7 @@ class flag_flag {
       $sid = flag_get_sid($uid, TRUE);
       // Anonymous users must always have a session id.
       if ($sid == 0 && $account->uid == 0) {
+        $this->errors['session'] = t('Internal error: You are anonymous but you have no session ID.');
         return FALSE;
       }
     }
@@ -1147,6 +1166,13 @@ class flag_flag {
   }
 
   /**
+   * Returns an array of errors set during validation.
+   */
+  function get_errors() {
+    return $this->errors;
+  }
+
+  /**
    * @addtogroup actions
    * @{
    * Methods that can be overridden to support Actions.
@@ -1394,7 +1420,7 @@ class flag_flag {
    *
    * For parameters documentation, see flag.tpl.php.
    */
-  function theme($action, $entity_id, $after_flagging = FALSE) {
+  function theme($action, $entity_id, $after_flagging = FALSE, $errors = array()) {
     static $js_added = array();
     global $user;
 
@@ -1426,7 +1452,7 @@ class flag_flag {
       }
     }
 
-    return theme($this->theme_suggestions(), array('flag' => $this, 'action' => $action, 'entity_id' => $entity_id, 'after_flagging' => $after_flagging));
+    return theme($this->theme_suggestions(), array('flag' => $this, 'action' => $action, 'entity_id' => $entity_id, 'after_flagging' => $after_flagging, 'errors' => $errors));
   }
 
   /**
diff --git a/theme/flag.css b/theme/flag.css
index 610b4b0..570d913 100644
--- a/theme/flag.css
+++ b/theme/flag.css
@@ -9,6 +9,12 @@
   font-size: .8em;
 }
 
+.flag-message.flag-failure-message {
+  border: 1px solid black;
+  background-color: #F5DCE6;
+  padding: 2px;
+}
+
 .flag-wrapper {
   position: relative;
 }
diff --git a/theme/flag.js b/theme/flag.js
index 86288f0..7de4658 100644
--- a/theme/flag.js
+++ b/theme/flag.js
@@ -43,7 +43,7 @@ Drupal.flagLink = function(context) {
     Drupal.attachBehaviors($newLink.get(0));
 
     $('.flag-message', $newLink).fadeIn();
-    setTimeout(function(){ $('.flag-message', $newLink).fadeOut() }, 3000);
+    setTimeout(function(){ $('.flag-message.flag-auto-remove', $newLink).fadeOut() }, 3000);
     return $newLink.get(0);
   }
 
@@ -78,20 +78,12 @@ Drupal.flagLink = function(context) {
       data: { js: true },
       dataType: 'json',
       success: function (data) {
-        if (data.status) {
-          // Success.
-          data.link = $wrapper.get(0);
-          $.event.trigger('flagGlobalBeforeLinkUpdate', [data]);
-          if (!data.preventDefault) { // A handler may cancel updating the link.
-            data.link = updateLink(element, data.newLink);
-          }
-          $.event.trigger('flagGlobalAfterLinkUpdate', [data]);
-        }
-        else {
-          // Failure.
-          alert(data.errorMessage);
-          $wrapper.removeClass('flag-waiting');
+        data.link = $wrapper.get(0);
+        $.event.trigger('flagGlobalBeforeLinkUpdate', [data]);
+        if (!data.preventDefault) { // A handler may cancel updating the link.
+          data.link = updateLink(element, data.newLink);
         }
+        $.event.trigger('flagGlobalAfterLinkUpdate', [data]);
       },
       error: function (xmlhttp) {
         alert('An HTTP error '+ xmlhttp.status +' occurred.\n'+ element.href);
diff --git a/theme/flag.tpl.php b/theme/flag.tpl.php
index d086f82..df6bfdc 100644
--- a/theme/flag.tpl.php
+++ b/theme/flag.tpl.php
@@ -21,6 +21,8 @@
  * - $link_title: The title attribute for the link.
  *
  * - $message_text: The long message to show after a flag action has been carried out.
+ * - $message_classes: A space-separated list of CSS classes that should be applied to
+ *   the message.
  * - $after_flagging: This template is called for the link both before and after being
  *   flagged. If displaying to the user immediately after flagging, this value
  *   will be boolean TRUE. This is usually used in conjunction with immedate
@@ -42,7 +44,7 @@
     <span class="<?php print $flag_classes ?>"><?php print $link_text; ?></span>
   <?php endif; ?>
   <?php if ($after_flagging): ?>
-    <span class="flag-message flag-<?php print $status; ?>-message">
+    <span class="<?php print $message_classes; ?>">
       <?php print $message_text; ?>
     </span>
   <?php endif; ?>
