diff --git a/includes/form.inc b/includes/form.inc
index 0e52124441ecfc338ff1a20b3b46c1d6a4a58414..13571a111f9f0a87cfbecac6feea204dd3421b46 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -888,19 +888,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
       _form_validate($elements[$key], $form_state);
     }
   }
+
   // Validate the current input.
   if (!isset($elements['#validated']) || !$elements['#validated']) {
+    // The following errors are always shown.
     if (isset($elements['#needs_validation'])) {
-      // Make sure a value is passed when the field is required.
-      // A simple call to empty() will not cut it here as some fields, like
-      // checkboxes, can return a valid value of '0'. Instead, check the
-      // length if it's a string, and the item count if it's an array.
-      // An unchecked checkbox has a #value of numeric 0, different than string
-      // '0', which could be a valid value.
-      if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
-        form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
-      }
-
       // Verify that the value is not longer than #maxlength.
       if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) {
         form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value']))));
@@ -929,6 +921,36 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
       }
     }
 
+    // While this element is being validated, it may be desired that some calls
+    // to form_set_error() be suppressed and not result in a form error, so
+    // that a button that implements low-risk functionality (such as "Previous"
+    // or "Add more") that doesn't require all user input to be valid can still
+    // have its submit handlers triggered. The clicked button's
+    // #limit_validation_errors property contains the information for which
+    // errors are needed, and all other errors are to be suppressed. The
+    // #limit_validation_errors property is ignored if the button doesn't also
+    // define its own submit handlers, because it's too large a security risk to
+    // have any invalid user input when executing form-level submit handlers.
+    if (isset($form_state['clicked_button']['#limit_validation_errors']) && isset($form_state['clicked_button']['#submit'])) {
+      form_set_error(NULL, '', $form_state['clicked_button']['#limit_validation_errors']);
+    }
+    else {
+      // As an extra security measure, explicitly turn off error suppression.
+      // Since this is also done at the end of this function, doing it here is
+      // only to handle the rare edge case where a validate handler invokes form
+      // processing of another form.
+      drupal_static_reset('form_set_error:limit_validation_errors');
+    }
+    // Make sure a value is passed when the field is required.
+    // A simple call to empty() will not cut it here as some fields, like
+    // checkboxes, can return a valid value of '0'. Instead, check the
+    // length if it's a string, and the item count if it's an array.
+    // An unchecked checkbox has a #value of numeric 0, different than string
+    // '0', which could be a valid value.
+    if (isset($elements['#needs_validation']) && $elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
+      form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
+    }
+
     // Call user-defined form level validators.
     if (isset($form_id)) {
       form_execute_handlers('validate', $elements, $form_state);
@@ -944,6 +966,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
     }
     $elements['#validated'] = TRUE;
   }
+
+  // Done validating this element, so turn off error suppression.
+  // _form_validate() turns it on again when starting on the next element, if
+  // it's still appropriate to do so.
+  drupal_static_reset('form_set_error:limit_validation_errors');
 }
 
 /**
@@ -1005,18 +1032,99 @@ function form_execute_handlers($type, &$form, &$form_state) {
  *   element where the #parents array starts with 'foo'.
  * @param $message
  *   The error message to present to the user.
+ * @param $limit_validation_errors
+ *   Internal use only. The #limit_validation_errors property of the clicked
+ *   button if it exists. Multistep forms not wanting to validate the whole form
+ *   can set the #limit_validation_errors property on buttons to avoid
+ *   validation errors of some elements preventing the button's submit handlers
+ *   from running. For example, pressing the "Previous" button should not fire
+ *   validation errors just because the current step has invalid values. AJAX is
+ *   another typical example.
+ *   If this property is set on the clicked button, the button must also define
+ *   its #submit property and those handlers will be executed even if there is
+ *   invalid input, so extreme care should be taken with respect to what is
+ *   performed by them. This is typically not a problem with buttons like
+ *   "Previous" or "Add more" that do not invoke persistent storage of the
+ *   submitted form values.
+ *   Do not use the #limit_validation_errors property on buttons that trigger
+ *   saving of form values to the database.
+ *   The #limit_validation_errors property is a list of "sections" within
+ *   $form_state['values'] that must contain valid values. Each "section" is an
+ *   array with the ordered set of keys needed to reach that part of
+ *   $form_state['values'] (i.e., the #parents property of the element).
+ *   For example:
+ *   @code
+ *     $form['actions']['previous']['#limit_validation_errors'] = array(
+ *       array('step1'),
+ *       array('foo', 'bar'),
+ *     );
+ *   @endcode
+ *   This will require $form_state['values']['step1'] and everything within it
+ *   (for example, $form_state['values']['step1']['choice']) to be valid, so
+ *   calls to form_set_error('step1', $message) or
+ *   form_set_error('step1][choice', $message) will prevent the submit handlers
+ *   from running, and result in the error message being displayed to the user.
+ *   However, calls to form_set_error('step2', $message) and
+ *   form_set_error('step2][groupX][choiceY', $message) will be suppressed,
+ *   resulting in the message not being displayed to the user, and the submit
+ *   handlers will run despite $form_state['values']['step2'] and
+ *   $form_state['values']['step2']['groupX']['choiceY'] containing invalid
+ *   values. Errors for an invalid $form_state['values']['foo'] will be
+ *   suppressed, but errors for invalid values for
+ *   $form_state['values']['foo']['bar'] and everything within it will be
+ *   recorded. If the button doesn't need any user input to be valid, then the
+ *   #limit_validation_errors can be set to an empty array, in which case, all
+ *   calls to form_set_error() will be suppressed.
+ *   Partial form validation is implemented by suppressing errors rather than by
+ *   skipping the input processing and validation steps entirely, because some
+ *   forms have button-level submit handlers that call Drupal API functions that
+ *   assume that certain data exists within $form_state['values'], and while not
+ *   doing anything with that data that requires it to be valid, PHP errors
+ *   would be triggered if the input processing and validation steps were fully
+ *   skipped. @see http://drupal.org/node/370537.
+ *
  * @return
- *   Return value is for internal use only. To get a list of errors, use 
+ *   Return value is for internal use only. To get a list of errors, use
  *   form_get_errors() or form_get_error().
  */
-function form_set_error($name = NULL, $message = '') {
+function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) {
   $form = &drupal_static(__FUNCTION__, array());
+  $sections = &drupal_static(__FUNCTION__ . ':limit_validation_errors');
+  if (isset($limit_validation_errors)) {
+    $sections = $limit_validation_errors;
+  }
+
   if (isset($name) && !isset($form[$name])) {
-    $form[$name] = $message;
-    if ($message) {
-      drupal_set_message($message, 'error');
+    $record = TRUE;
+    if (isset($sections)) {
+      // #limit_validation_errors is an array of "sections" within which user
+      // input must be valid. If the element is within one of these sections,
+      // the error must be recorded. Otherwise, it can be suppressed.
+      // #limit_validation_errors can be an empty array, in which case all
+      // errors are suppressed. For example, a "Previous" button might want its
+      // submit action to be triggered even if none of the submitted values are
+      // valid.
+      $record = FALSE;
+      foreach ($sections as $section) {
+        // Exploding by '][' reconstructs the element's #parents. If the
+        // reconstructed #parents begin with the same keys as the specified
+        // section, then the element's values are within the part of
+        // $form_state['values'] that the clicked button requires to be valid,
+        // so errors for this element must be recorded.
+        if (array_slice(explode('][', $name), 0, count($section)) === $section) {
+          $record = TRUE;
+          break;
+        }
+      }
+    }
+    if ($record) {
+      $form[$name] = $message;
+      if ($message) {
+        drupal_set_message($message, 'error');
+      }
     }
   }
+
   return $form;
 }
 
@@ -3261,7 +3369,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd
   $batch =& batch_get();
 
   drupal_theme_initialize();
-  
+
   if (isset($batch)) {
     // Add process information
     $process_info = array(
@@ -3276,7 +3384,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd
     );
     $batch += $process_info;
 
-    // The batch is now completely built. Allow other modules to make changes to the 
+    // The batch is now completely built. Allow other modules to make changes to the
     // batch so that it is easier to reuse batch processes in other enviroments.
     drupal_alter('batch', $batch);
 
diff --git a/modules/field/field.form.inc b/modules/field/field.form.inc
index 0970c1a6a82ccabd6c063678dca19755cbabcc3c..a2c03a5c1d59291b84534dc8b96bd482783d5b3a 100644
--- a/modules/field/field.form.inc
+++ b/modules/field/field.form.inc
@@ -214,7 +214,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form,
           '#name' => $field_name . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
-          // Submit callback for disabled JavaScript.
+          '#limit_validation_errors' => array(array($field_name, $langcode)),
           '#submit' => array('field_add_more_submit'),
           '#ajax' => array(
             'callback' => 'field_add_more_js',
@@ -341,9 +341,13 @@ function field_default_form_errors($obj_type, $object, $field, $instance, $langc
 }
 
 /**
- * Submit handler to add more choices to a field form. This handler is used when
- * JavaScript is not available. It makes changes to the form state and the
- * entire form is rebuilt during the page reload.
+ * Submit handler for the "Add another item" button of a field form.
+ *
+ * This handler is run regardless of whether JS is enabled or not. It makes
+ * changes to the form state. If the button was clicked with JS disabled, then
+ * the page is reloaded with the complete rebuilt form. If the button was
+ * clicked with JS enabled, then ajax_form_callback() calls field_add_more_js()
+ * to return just the changed part of the form.
  */
 function field_add_more_submit($form, &$form_state) {
   // Set the form to rebuild and run submit handlers.
@@ -360,7 +364,12 @@ function field_add_more_submit($form, &$form_state) {
 }
 
 /**
- * Ajax callback for addition of new empty widgets.
+ * Ajax callback in response to a new empty widget being added to the form.
+ *
+ * This returns the new page content to replace the page content made obsolete
+ * by the form submission.
+ *
+ * @see field_add_more_submit()
  */
 function field_add_more_js($form, $form_state) {
   // Retrieve field information.
diff --git a/modules/poll/poll.module b/modules/poll/poll.module
index 9c48de6b6154d849f86eab4a99d046e680617299..42d7afc68cde7da1f26a1a8e5402c85b75629bf8 100644
--- a/modules/poll/poll.module
+++ b/modules/poll/poll.module
@@ -271,7 +271,8 @@ function poll_form($node, &$form_state) {
     '#value' => t('More choices'),
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
-    '#submit' => array('poll_more_choices_submit'), // If no javascript action.
+    '#limit_validation_errors' => array(array('choice')),
+    '#submit' => array('poll_more_choices_submit'),
     '#ajax' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
@@ -322,9 +323,13 @@ function poll_form($node, &$form_state) {
 }
 
 /**
- * Submit handler to add more choices to a poll form. This handler is used when
- * javascript is not available. It makes changes to the form state and the
- * entire form is rebuilt during the page reload.
+ * Submit handler to add more choices to a poll form.
+ *
+ * This handler is run regardless of whether JS is enabled or not. It makes
+ * changes to the form state. If the button was clicked with JS disabled, then
+ * the page is reloaded with the complete rebuilt form. If the button was
+ * clicked with JS enabled, then ajax_form_callback() calls poll_choice_js() to
+ * return just the changed part of the form.
  */
 function poll_more_choices_submit($form, &$form_state) {
   include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc';
@@ -379,7 +384,12 @@ function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight
 }
 
 /**
- * Menu callback for AHAH additions. Render the new poll choices.
+ * Ajax callback in response to new choices being added to the form.
+ *
+ * This returns the new page content to replace the page content made obsolete
+ * by the form submission.
+ *
+ * @see poll_more_choices_submit()
  */
 function poll_choice_js($form, $form_state) {
   return $form['choice_wrapper']['choice'];
diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index ba6a5d622c7b3efff2af7e73fc9885f42cc805d2..10839177757bbf99b757b15c29e20d6322418812 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -231,6 +231,27 @@ class FormValidationTestCase extends DrupalWebTestCase {
     $this->assertNoFieldByName('name', t('Form element was hidden.'));
     $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.'));
   }
+
+  /**
+   * Tests partial form validation through #limit_validation_errors.
+   */
+  function testValidateLimitErrors() {
+    $edit = array('test' => 'invalid');
+    $path = 'form-test/limit-validation-errors';
+
+    // Submit the form by pressing the button with #limit_validation_errors and
+    // ensure that the title field is not validated, but the #element_validate
+    // handler for the 'test' field is triggered.
+    $this->drupalPost($path, $edit, t('Partial validate'));
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+
+    // Now test full form validation and ensure that the #element_validate
+    // handler is still triggered.
+    $this->drupalPost($path, $edit, t('Full validate'));
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+  }
 }
 
 /**
diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module
index 2036204ba26189157ee30838c00319e13f0c0f77..c8f565938361a572a880e9aa39b0481a9773d344 100644
--- a/modules/simpletest/tests/form_test.module
+++ b/modules/simpletest/tests/form_test.module
@@ -17,6 +17,13 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/limit-validation-errors'] = array(
+    'title' => 'Form validation with some error suppression',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_limit_validation_errors_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -203,6 +210,41 @@ function form_test_validate_form_validate(&$form, &$form_state) {
   }
 }
 
+/**
+ * Builds a simple form with a button triggering partial validation.
+ */
+function form_test_limit_validation_errors_form($form, &$form_state) {
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['test'] = array(
+    '#type' => 'textfield',
+    '#element_validate' => array('form_test_limit_validation_errors_element_validate_test'),
+  );
+  $form['actions']['partial'] = array(
+    '#type' => 'submit',
+    '#limit_validation_errors' => array(array('test')),
+    '#submit' => array(),
+    '#value' => t('Partial validate'),
+  );
+  $form['actions']['full'] = array(
+    '#type' => 'submit',
+    '#value' => t('Full validate'),
+  );
+  return $form;
+}
+
+/**
+ * Form element validation handler for the 'test' element.
+ */
+function form_test_limit_validation_errors_element_validate_test(&$element, &$form_state) {
+  if ($element['#value'] == 'invalid') {
+    form_error($element, 'Test element is invalid');
+  }
+}
+
 /**
  * Create a header and options array. Helper function for callbacks.
  */
@@ -895,7 +937,7 @@ function form_test_state_persist($form, &$form_state) {
 
 /**
  * Submit handler.
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_state_persist_submit($form, &$form_state) {
@@ -905,7 +947,7 @@ function form_test_state_persist_submit($form, &$form_state) {
 
 /**
  * Implements hook_form_FORM_ID_alter().
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_form_form_test_state_persist_alter(&$form, &$form_state) {