diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 8bd33b7..53845fc 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -442,6 +442,9 @@ function _batch_finished() {
     }
 
     // Determine the target path to redirect to.
+    if (!isset($_batch['form_state'])) {
+      $_batch['form_state'] = new FormState();
+    }
     if (!isset($_batch['form_state']['redirect'])) {
       if (isset($_batch['redirect'])) {
         $_batch['form_state']['redirect'] = $_batch['redirect'];
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 214ce28..2063444 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -804,12 +804,12 @@ function install_tasks_to_display($install_state) {
 function install_get_form($form_id, array &$install_state) {
   // Ensure the form will not redirect, since install_run_tasks() uses a custom
   // redirection logic.
-  $form_state = array(
+  $form_state = new FormState(array(
     'build_info' => array(
       'args' => array(&$install_state),
     ),
     'no_redirect' => TRUE,
-  );
+  ));
   $form_builder = \Drupal::formBuilder();
   if ($install_state['interactive']) {
     $form = $form_builder->buildForm($form_id, $form_state);
diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php
index a8c44e5..da3d7e4 100644
--- a/core/lib/Drupal/Core/Controller/FormController.php
+++ b/core/lib/Drupal/Core/Controller/FormController.php
@@ -66,7 +66,7 @@ public function getContentResult(Request $request) {
 
     // Add the form and form_state to trick the getArguments method of the
     // controller resolver.
-    $form_state = array();
+    $form_state = new FormState();
     $request->attributes->set('form', array());
     $request->attributes->set('form_state', $form_state);
     $args = $this->controllerResolver->getArguments($request, array($form_object, 'buildForm'));
diff --git a/core/lib/Drupal/Core/Entity/EntityFormBuilder.php b/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
index c875754..e7921b6 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
@@ -44,10 +44,11 @@ public function __construct(EntityManagerInterface $entity_manager, FormBuilderI
   /**
    * {@inheritdoc}
    */
-  public function getForm(EntityInterface $entity, $operation = 'default', array $form_state = array()) {
+  public function getForm(EntityInterface $entity, $operation = 'default', $form_state_additions = array()) {
     $form_object = $this->entityManager->getFormObject($entity->getEntityTypeId(), $operation);
     $form_object->setEntity($entity);
 
+    $form_state = new FormState($form_state_additions);
     $form_state['build_info']['callback_object'] = $form_object;
     $form_state['build_info']['base_form_id'] = $form_object->getBaseFormID();
     $form_state['build_info'] += array('args' => array());
diff --git a/core/lib/Drupal/Core/Entity/EntityFormBuilderInterface.php b/core/lib/Drupal/Core/Entity/EntityFormBuilderInterface.php
index 888abdb..d980758 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormBuilderInterface.php
@@ -24,9 +24,9 @@
    *   _entity_form: node.book_outline
    *   @endcode
    *   where "book_outline" is the value of $operation.
-   * @param array $form_state
-   *   (optional) An associative array containing the current state of the form.
-   *   Use this to pass additional information to the form, such as the
+   * @param array $form_state_additions
+   *   (optional) An associative array used to build the current state of the
+   *   form. Use this to pass additional information to the form, such as the
    *   langcode. Defaults to an empty array.
    *
    * @code
@@ -37,6 +37,6 @@
    * @return array
    *   The processed form for the given entity and operation.
    */
-  public function getForm(EntityInterface $entity, $operation = 'default', array $form_state = array());
+  public function getForm(EntityInterface $entity, $operation = 'default', $form_state_additions = array());
 
 }
diff --git a/core/lib/Drupal/Core/Form/FormBase.php b/core/lib/Drupal/Core/Form/FormBase.php
index 8848d3f..eb8efa2 100644
--- a/core/lib/Drupal/Core/Form/FormBase.php
+++ b/core/lib/Drupal/Core/Form/FormBase.php
@@ -45,13 +45,6 @@
   protected $configFactory;
 
   /**
-   * The form error handler.
-   *
-   * @var \Drupal\Core\Form\FormErrorInterface
-   */
-  protected $errorHandler;
-
-  /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
@@ -172,34 +165,21 @@ private function container() {
   }
 
   /**
-   * Returns the form error handler.
-   *
-   * @return \Drupal\Core\Form\FormErrorInterface
-   *   The form error handler.
-   */
-  protected function errorHandler() {
-    if (!$this->errorHandler) {
-      $this->errorHandler = \Drupal::service('form_builder');
-    }
-    return $this->errorHandler;
-  }
-
-  /**
    * Files an error against a form element.
    *
    * @param string $name
    *   The name of the form element.
-   * @param array $form_state
-   *   An associative array containing the current state of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
    * @param string $message
    *   (optional) The error message to present to the user.
    *
-   * @see \Drupal\Core\Form\FormErrorInterface::setErrorByName()
+   * @deprecated Use \Drupal\Core\Form\FormStateInterface::setErrorByName().
    *
    * @return $this
    */
-  protected function setFormError($name, array &$form_state, $message = '') {
-    $this->errorHandler()->setErrorByName($name, $form_state, $message);
+  protected function setFormError($name, FormStateInterface $form_state, $message = '') {
+    $form_state->setErrorByName($name, $message);
     return $this;
   }
 
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index db2900c..137e4fb 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -157,7 +157,7 @@ public function getFormId($form_arg, &$form_state) {
    * {@inheritdoc}
    */
   public function getForm($form_arg) {
-    $form_state = array();
+    $form_state = new FormState();
 
     $args = func_get_args();
     // Remove $form_arg from the arguments.
@@ -170,10 +170,7 @@ public function getForm($form_arg) {
   /**
    * {@inheritdoc}
    */
-  public function buildForm($form_id, array &$form_state) {
-    // Ensure some defaults; if already set they will not be overridden.
-    $form_state += $this->getFormStateDefaults();
-
+  public function buildForm($form_id, FormStateInterface &$form_state) {
     // Ensure the form ID is prepared.
     $form_id = $this->getFormId($form_id, $form_state);
 
@@ -208,7 +205,7 @@ public function buildForm($form_id, array &$form_state) {
       // keys need to be removed after retrieving and preparing the form, except
       // any that were already set prior to retrieving the form.
       if ($check_cache) {
-        $form_state_before_retrieval = $form_state;
+        $form_state_before_retrieval = clone $form_state;
       }
 
       $form = $this->retrieveForm($form_id, $form_state);
@@ -229,9 +226,9 @@ public function buildForm($form_id, array &$form_state) {
       // - temporary: Any assigned data is expected to survives within the same
       //   page request.
       if ($check_cache) {
-        $uncacheable_keys = array_flip(array_diff($this->getUncacheableKeys(), array('always_process', 'temporary')));
-        $form_state = array_diff_key($form_state, $uncacheable_keys);
-        $form_state += $form_state_before_retrieval;
+        $cache_form_state = $form_state->getCacheableArray(array('always_process', 'temporary'));
+        $form_state = $form_state_before_retrieval;
+        $form_state->setFormState($cache_form_state);
       }
     }
 
@@ -348,7 +345,7 @@ public function getCache($form_build_id, &$form_state) {
       if ((isset($form['#cache_token']) && $this->csrfToken->validate($form['#cache_token'])) || (!isset($form['#cache_token']) && $user->isAnonymous())) {
         if ($stored_form_state = $this->keyValueExpirableFactory->get('form_state')->get($form_build_id)) {
           // Re-populate $form_state for subsequent rebuilds.
-          $form_state = $stored_form_state + $form_state;
+          $form_state->setFormState($stored_form_state);
 
           // If the original form is contained in include files, load the files.
           // @see form_load_include()
@@ -397,44 +394,12 @@ public function setCache($form_build_id, $form, $form_state) {
     //   https://www.drupal.org/node/2295823
     $form_state['build_info']['safe_strings'] = SafeMarkup::getAll();
 
-    if ($data = array_diff_key($form_state, array_flip($this->getUncacheableKeys()))) {
+    if ($data = $form_state->getCacheableArray()) {
       $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire);
     }
   }
 
   /**
-   * Returns an array of $form_state keys that shouldn't be cached.
-   */
-  protected function getUncacheableKeys() {
-    return array(
-      // Public properties defined by form constructors and form handlers.
-      'always_process',
-      'must_validate',
-      'rebuild',
-      'rebuild_info',
-      'redirect',
-      'redirect_route',
-      'no_redirect',
-      'temporary',
-      // Internal properties defined by form processing.
-      'buttons',
-      'triggering_element',
-      'complete_form',
-      'groups',
-      'input',
-      'method',
-      'submit_handlers',
-      'validation_complete',
-      'submitted',
-      'executed',
-      'validate_handlers',
-      'values',
-      'errors',
-      'limit_validation_errors',
-    );
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function submitForm($form_arg, &$form_state) {
@@ -444,8 +409,6 @@ public function submitForm($form_arg, &$form_state) {
       unset($args[0], $args[1]);
       $form_state['build_info']['args'] = array_values($args);
     }
-    // Merge in default values.
-    $form_state += $this->getFormStateDefaults();
 
     // Populate $form_state['input'] with the submitted values before retrieving
     // the form, to be consistent with what self::buildForm() does for
@@ -824,7 +787,7 @@ public function doBuildForm($form_id, &$element, &$form_state) {
       // Store a reference to the complete form in $form_state prior to building
       // the form. This allows advanced #process and #after_build callbacks to
       // perform changes elsewhere in the form.
-      $form_state['complete_form'] = &$element;
+      $form_state->setCompleteForm($element);
 
       // Set a flag if we have a correct form submission. This is always TRUE
       // for programmed forms coming from self::submitForm(), or if the form_id
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
new file mode 100644
index 0000000..6b21377
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -0,0 +1,696 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormState.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Url;
+
+/**
+ * Stores information about the state of a form.
+ *
+ * @todo Remove usage of \ArrayAccess.
+ */
+class FormState implements FormStateInterface, \ArrayAccess {
+
+  /**
+   * Tracks if any errors have been set on any form.
+   *
+   * @var bool
+   */
+  protected static $anyErrors = FALSE;
+
+  /**
+   * The internal storage of the form state.
+   *
+   * @var array
+   */
+  protected $internalStorage = array();
+
+  /**
+   * The complete form structure.
+   *
+   * #process, #after_build, #element_validate, and other handlers being invoked
+   * on a form element may use this reference to access other information in the
+   * form the element is contained in.
+   *
+   * @see self::getCompleteForm()
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $complete_form;
+
+  /**
+   * An associative array of information stored by Form API that is necessary to
+   * build and rebuild the form from cache when the original context may no
+   * longer be available:
+   *   - callback: The actual callback to be used to retrieve the form array.
+   *     Can be any callable. If none is provided $form_id is used as the name
+   *     of a function to call instead.
+   *   - args: A list of arguments to pass to the form constructor.
+   *   - files: An optional array defining include files that need to be loaded
+   *     for building the form. Each array entry may be the path to a file or
+   *     another array containing values for the parameters 'type', 'module' and
+   *     'name' as needed by module_load_include(). The files listed here are
+   *     automatically loaded by form_get_cache(). By default the current menu
+   *     router item's 'file' definition is added, if any. Use
+   *     form_load_include() to add include files from a form constructor.
+   *   - form_id: Identification of the primary form being constructed and
+   *     processed.
+   *   - base_form_id: Identification for a base form, as declared in the form
+   *     class's \Drupal\Core\Form\BaseFormIdInterface::getBaseFormId() method.
+   *
+   * @var array
+   */
+  protected $build_info = array(
+    'args' => array(),
+    'files' => array(),
+  );
+
+  /**
+   * Similar to self::$build_info, but pertaining to
+   * \Drupal\Core\Form\FormBuilderInterface::rebuildForm().
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $rebuild_info = array();
+
+  /**
+   * Normally, after the entire form processing is completed and submit handlers
+   * have run, a form is considered to be done and
+   * \Drupal\Core\Form\FormSubmitterInterface::redirectForm() will redirect the
+   * user to a new page using a GET request (so a browser refresh does not
+   * re-submit the form). However, if 'rebuild' has been set to TRUE, then a new
+   * copy of the form is immediately built and sent to the browser, instead of a
+   * redirect. This is used for multi-step forms, such as wizards and
+   * confirmation forms. Normally, $form_state['rebuild'] is set by a submit
+   * handler, since its is usually logic within a submit handler that determines
+   * whether a form is done or requires another step. However, a validation
+   * handler may already set $form_state['rebuild'] to cause the form processing
+   * to bypass submit handlers and rebuild the form instead, even if there are
+   * no validation errors.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $rebuild = FALSE;
+
+  /**
+   * Used when a form needs to return some kind of a
+   * \Symfony\Component\HttpFoundation\Response object, e.g., a
+   * \Symfony\Component\HttpFoundation\BinaryFileResponse when triggering a
+   * file download. If you use the $form_state['redirect'] key, it will be used
+   * to build a \Symfony\Component\HttpFoundation\RedirectResponse and will
+   * populate this key.
+   *
+   * @var \Symfony\Component\HttpFoundation\Response|null
+   */
+  protected $response;
+
+  /**
+   * Used to redirect the form on submission. It may either be a  string
+   * containing the destination URL, or an array of arguments compatible with
+   * url(). See url() for complete information.
+   *
+   * This property is uncacheable.
+   *
+   * @var string|array|null
+   */
+  protected $redirect;
+
+  /**
+   * Used for route-based redirects.
+   *
+   * This property is uncacheable.
+   *
+   * @var \Drupal\Core\Url|array
+   */
+  protected $redirect_route;
+
+  /**
+   * If set to TRUE the form will NOT perform a redirect, even if
+   * self::$redirect is set.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $no_redirect;
+
+  /**
+   * The HTTP form method to use for finding the input for this form.
+   *
+   * May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method forms do
+   * not use form ids so are always considered to be submitted, which can have
+   * unexpected effects. The 'get' method should only be used on forms that do
+   * not change data, as that is exclusively the domain of 'post.'
+   *
+   * This property is uncacheable.
+   *
+   * @var string
+   */
+  protected $method = 'post';
+
+  /**
+   * If set to TRUE the original, unprocessed form structure will be cached,
+   * which allows the entire form to be rebuilt from cache. A typical form
+   * workflow involves two page requests; first, a form is built and rendered
+   * for the user to fill in. Then, the user fills the form in and submits it,
+   * triggering a second page request in which the form must be built and
+   * processed. By default, $form and $form_state are built from scratch during
+   * each of these page requests. Often, it is necessary or desired to persist
+   * the $form and $form_state variables from the initial page request to the
+   * one that processes the submission. 'cache' can be set to TRUE to do this.
+   * A prominent example is an Ajax-enabled form, in which ajax_process_form()
+   * enables form caching for all forms that include an element with the #ajax
+   * property. (The Ajax handler has no way to build the form itself, so must
+   * rely on the cached version.) Note that the persistence of $form and
+   * $form_state happens automatically for (multi-step) forms having the
+   * self::$rebuild flag set, regardless of the value for self::$cache.
+   *
+   * @var bool
+   */
+  protected $cache = FALSE;
+
+  /**
+   * If set to TRUE the form will NOT be cached, even if 'cache' is set.
+   *
+   * @var bool
+   */
+  protected $no_cache;
+
+  /**
+   * An associative array of values submitted to the form.
+   *
+   * The validation functions and submit functions use this array for nearly all
+   * their decision making. (Note that #tree determines whether the values are a
+   * flat array or an array whose structure parallels the $form array. See the
+   * @link forms_api_reference.html Form API reference @endlink for more
+   * information.)
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $values;
+
+  /**
+   * The array of values as they were submitted by the user.
+   *
+   * These are raw and unvalidated, so should not be used without a thorough
+   * understanding of security implications. In almost all cases, code should
+   * use the data in the 'values' array exclusively. The most common use of this
+   * key is for multi-step forms that need to clear some of the user input when
+   * setting 'rebuild'. The values correspond to \Drupal::request()->request or
+   * \Drupal::request()->query, depending on the 'method' chosen.
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $input;
+
+  /**
+   * If TRUE and the method is GET, a form_id is not necessary.
+   *
+   * This should only be used on RESTful GET forms that do NOT write data, as
+   * this could lead to security issues. It is useful so that searches do not
+   * need to have a form_id in their query arguments to trigger the search.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $always_process;
+
+  /**
+   * Ordinarily, a form is only validated once, but there are times when a form
+   * is resubmitted internally and should be validated again. Setting this to
+   * TRUE will force that to happen. This is most likely to occur during Ajax
+   * operations.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $must_validate;
+
+  /**
+   * If TRUE, the form was submitted programmatically, usually invoked via
+   * \Drupal\Core\Form\FormBuilderInterface::submitForm(). Defaults to FALSE.
+   *
+   * @var bool
+   */
+  protected $programmed = FALSE;
+
+  /**
+   * If TRUE, programmatic form submissions are processed without taking #access
+   * into account. Set this to FALSE when submitting a form programmatically
+   * with values that may have been input by the user executing the current
+   * request; this will cause #access to be respected as it would on a normal
+   * form submission. Defaults to TRUE.
+   *
+   * @var bool
+   */
+  protected $programmed_bypass_access_check = TRUE;
+
+  /**
+   * TRUE signifies correct form submission. This is always TRUE for programmed
+   * forms coming from \Drupal\Core\Form\FormBuilderInterface::submitForm() (see
+   * 'programmed' key), or if the form_id coming from the
+   * \Drupal::request()->request data is set and matches the current form_id.
+   *
+   * @var bool
+   */
+  protected $process_input;
+
+  /**
+   * If TRUE, the form has been submitted. Defaults to FALSE.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $submitted = FALSE;
+
+  /**
+   * If TRUE, the form was submitted and has been processed and executed.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $executed = FALSE;
+
+  /**
+   * The form element that triggered submission, which may or may not be a
+   * button (in the case of Ajax forms). This key is often used to distinguish
+   * between various buttons in a submit handler, and is also used in Ajax
+   * handlers.
+   *
+   * This property is uncacheable.
+   *
+   * @var array|null
+   */
+  protected $triggering_element;
+
+  /**
+   * If TRUE, there is a file element and Form API will set the appropriate
+   * 'enctype' HTML attribute on the form.
+   *
+   * @var bool
+   */
+  protected $has_file_element;
+
+  /**
+   * Contains references to details elements to render them within vertical tabs.
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $groups = array();
+
+  /**
+   *  This is not a special key, and no specific support is provided for it in
+   *  the Form API. By tradition it was the location where application-specific
+   *  data was stored for communication between the submit, validation, and form
+   *  builder functions, especially in a multi-step-style form. Form
+   *  implementations may use any key(s) within $form_state (other than the keys
+   *  listed here and other reserved ones used by Form API internals) for this
+   *  kind of storage. The recommended way to ensure that the chosen key doesn't
+   *  conflict with ones used by the Form API or other modules is to use the
+   *  module name as the key name or a prefix for the key name. For example, the
+   *  entity form classes use $this->entity in entity forms, or
+   *  $form_state['controller']->getEntity() outside the controller, to store
+   *  information about the entity being edited, and this information stays
+   *  available across successive clicks of the "Preview" button (if available)
+   *  as well as when the "Save" button is finally clicked.
+   *
+   * @var array
+   */
+  protected $storage = array();
+
+  /**
+   * A list containing copies of all submit and button elements in the form.
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $buttons = array();
+
+  /**
+   * Holds temporary data accessible during the current page request only.
+   *
+   * All $form_state properties that are not reserved keys (see
+   * other properties marked as uncacheable) persist throughout a multistep form
+   * sequence. Form API provides this key for modules to communicate information
+   * across form-related functions during a single page request. It may be used
+   * to temporarily save data that does not need to or should not be cached
+   * during the whole form workflow; e.g., data that needs to be accessed during
+   * the current form build process only. There is no use-case for this
+   * functionality in Drupal core.
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $temporary;
+
+  /**
+   * Tracks if the form has finished validation.
+   *
+   * This property is uncacheable.
+   *
+   * @var bool
+   */
+  protected $validation_complete = FALSE;
+
+  /**
+   * Contains errors for this form.
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $errors = array();
+
+  /**
+   * Stores which errors should be limited during validation.
+   *
+   * This property is uncacheable.
+   *
+   * @var array|null
+   */
+  protected $limit_validation_errors;
+
+  /**
+   * Stores the gathered validation handlers.
+   *
+   * This property is uncacheable.
+   *
+   * @var array|null
+   */
+  protected $validate_handlers;
+
+  /**
+   * Stores the gathered submission handlers.
+   *
+   * This property is uncacheable.
+   *
+   * @var array|null
+   */
+  protected $submit_handlers;
+
+  /**
+   * @param array $form_state_additions
+   */
+  public function __construct(array $form_state_additions = array()) {
+    $this->setFormState($form_state_additions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFormState(array $form_state_additions) {
+    foreach ($form_state_additions as $key => $value) {
+      $this->set($key, $value);
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableArray($allowed_keys = array()) {
+    $cacheable_array = array(
+      'build_info' => $this->build_info,
+      'response' => $this->response,
+      'cache' => $this->cache,
+      'no_cache' => $this->no_cache,
+      'programmed' => $this->programmed,
+      'programmed_bypass_access_check' => $this->programmed_bypass_access_check,
+      'process_input' => $this->process_input,
+      'has_file_element' => $this->has_file_element,
+      'storage' => $this->storage,
+    ) + $this->internalStorage;
+    foreach ($allowed_keys as $allowed_key) {
+      $cacheable_array[$allowed_key] = $this->get($allowed_key);
+    }
+    return $cacheable_array;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCompleteForm(array &$complete_form) {
+    $this->complete_form = &$complete_form;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getCompleteForm() {
+    return $this->complete_form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($offset) {
+    return isset($this->{$offset}) || isset($this->internalStorage[$offset]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &offsetGet($offset) {
+    $value = &$this->get($offset);
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetSet($offset, $value) {
+    $this->set($offset, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetUnset($offset) {
+    if (property_exists($this, $offset)) {
+      $this->{$offset} = NULL;
+    }
+    unset($this->internalStorage[$offset]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setIfNotExists($property, $value) {
+    if (property_exists($this, $property)) {
+      if ($this->{$property} === NULL) {
+        $this->set($property, $value);
+      }
+    }
+    else if (!array_key_exists($property, $this->internalStorage)) {
+      $this->set($property, $value);
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &get($property) {
+    if (property_exists($this, $property)) {
+      return $this->{$property};
+    }
+    else {
+      if (!isset($this->internalStorage[$property])) {
+        $this->internalStorage[$property] = NULL;
+      }
+      return $this->internalStorage[$property];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($property, $value) {
+    if (property_exists($this, $property)) {
+      $this->{$property} = $value;
+    }
+    else {
+      $this->internalStorage[$property] = $value;
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRedirect(Url $url) {
+    $this->set('redirect_route', $url);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRedirect() {
+    // Skip redirection for form submissions invoked via
+    // \Drupal\Core\Form\FormBuilderInterface::submitForm().
+    if (!empty($this['programmed'])) {
+      return FALSE;
+    }
+    // Skip redirection if rebuild is activated.
+    if (!empty($this['rebuild'])) {
+      return FALSE;
+    }
+    // Skip redirection if it was explicitly disallowed.
+    if (!empty($this['no_redirect'])) {
+      return FALSE;
+    }
+
+    // Check for a route-based redirection.
+    if (isset($this['redirect_route'])) {
+      // @todo Remove once all redirects are converted to Url.
+      if (!($this['redirect_route'] instanceof Url)) {
+        $this['redirect_route'] += array(
+          'route_parameters' => array(),
+          'options' => array(),
+        );
+        $this['redirect_route'] = new Url($this['redirect_route']['route_name'], $this['redirect_route']['route_parameters'], $this['redirect_route']['options']);
+      }
+
+      $this['redirect_route']->setAbsolute();
+      return $this['redirect_route'];
+    }
+
+    return $this['redirect'];
+  }
+
+  /**
+   * Sets the global status of errors.
+   *
+   * @param bool $errors
+   *   TRUE if any form has any errors, FALSE otherwise.
+   */
+  protected static function setAnyErrors($errors = TRUE) {
+    static::$anyErrors = $errors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function hasAnyErrors() {
+    return static::$anyErrors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setErrorByName($name, $message = '') {
+    if (!empty($this['validation_complete'])) {
+      throw new \LogicException('Form errors cannot be set after form validation has finished.');
+    }
+
+    if (!isset($this['errors'][$name])) {
+      $record = TRUE;
+      if (isset($this['limit_validation_errors'])) {
+        // #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 ($this['limit_validation_errors'] 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. As the exploded array
+          // will all be strings, we need to cast every value of the section
+          // array to string.
+          if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
+            $record = TRUE;
+            break;
+          }
+        }
+      }
+      if ($record) {
+        $this['errors'][$name] = $message;
+        static::setAnyErrors();
+        if ($message) {
+          $this->drupalSetMessage($message, 'error');
+        }
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setError(&$element, $message = '') {
+    $this->setErrorByName(implode('][', $element['#parents']), $message);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearErrors() {
+    $this->set('errors', array());
+    static::setAnyErrors(FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getError($element) {
+    if ($errors = $this->getErrors($this)) {
+      $parents = array();
+      foreach ($element['#parents'] as $parent) {
+        $parents[] = $parent;
+        $key = implode('][', $parents);
+        if (isset($errors[$key])) {
+          return $errors[$key];
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getErrors() {
+    return $this->get('errors');
+  }
+
+  /**
+   * Wraps drupal_set_message().
+   *
+   * @return array|null
+   */
+  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
+    return drupal_set_message($message, $type, $repeat);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
new file mode 100644
index 0000000..44fe619
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormStateInterface.php
@@ -0,0 +1,272 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormStateInterface.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Url;
+
+/**
+ * Provides an interface for an object containing the current state of a form.
+ *
+ * This is passed to all form related code so that the caller can use it to
+ * examine what in the form changed when the form submission process is
+ * complete. Furthermore, it may be used to store information related to the
+ * processed data in the form, which will persist across page requests when the
+ * 'cache' or 'rebuild' flag is set. See
+ * \Drupal\Core\Form\FormState::$internalStorage for documentation of the
+ * available flags.
+ *
+ * @see \Drupal\Core\Form\FormBuilderInterface
+ * @see \Drupal\Core\Form\FormValidatorInterface
+ * @see \Drupal\Core\Form\FormSubmitterInterface
+ */
+interface FormStateInterface {
+
+  /**
+   * Returns a reference to the complete form array.
+   *
+   * @return array
+   *   The complete form array.
+   */
+  public function &getCompleteForm();
+
+  /**
+   * Stores the complete form array.
+   *
+   * @param array $complete_form
+   *   The complete form array.
+   *
+   * @return $this
+   */
+  public function setCompleteForm(array &$complete_form);
+
+  /**
+   * Returns an array representation of the cacheable portion of the form state.
+   *
+   * @return array
+   *   The cacheable portion of the form state.
+   */
+  public function getCacheableArray();
+
+  /**
+   * Sets the value of the form state.
+   *
+   * @param array $form_state_additions
+   *   An array of values to add to the form state.
+   *
+   * @return $this
+   */
+  public function setFormState(array $form_state_additions);
+
+  /**
+   * Sets a value to an arbitrary property if it does not exist yet.
+   *
+   * @param string $property
+   *   The property to use for the value.
+   * @param mixed $value
+   *   The data to store.
+   *
+   * @return $this
+   */
+  public function setIfNotExists($property, $value);
+
+  /**
+   * Sets the redirect URL for the form.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The URL to redirect to.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Form\FormSubmitterInterface::redirectForm()
+   */
+  public function setRedirect(Url $url);
+
+  /**
+   * Gets the value to use for redirecting after the form has been executed.
+   *
+   * @see \Drupal\Core\Form\FormSubmitterInterface::redirectForm()
+   *
+   * @return mixed
+   *   The value will be one of the following:
+   *     - A fully prepared \Symfony\Component\HttpFoundation\RedirectResponse.
+   *     - An instance of \Drupal\Core\Url to use for the redirect.
+   *     - A numerically-indexed array where the first value is the path to use
+   *       for the redirect, and the optional second value is an array of
+   *       options for generating the URL from the path.
+   *     - The path to use for the redirect.
+   *     - NULL, to signify that no redirect was specified and that the current
+   *       path should be used for the redirect.
+   *     - FALSE, to signify that no redirect should take place.
+   */
+  public function getRedirect();
+
+  /**
+   * Gets any arbitrary property.
+   *
+   * @param string $property
+   *   The property to retrieve.
+   *
+   * @return mixed
+   *   The value for that property, or NULL if the property does not exist.
+   */
+  public function &get($property);
+
+  /**
+   * Sets a value to an arbitrary property.
+   *
+   * @param string $property
+   *   The property to use for the value.
+   * @param mixed $value
+   *   The value to set.
+   *
+   * @return $this
+   */
+  public function set($property, $value);
+
+  /**
+   * Determines if any forms have any errors.
+   *
+   * @return bool
+   *   TRUE if any form has any errors, FALSE otherwise.
+   */
+  public static function hasAnyErrors();
+
+  /**
+   * Files an error against a form element.
+   *
+   * When a validation error is detected, the validator calls this method to
+   * indicate which element needs to be changed and provide an error message.
+   * This causes the Form API to not execute the form submit handlers, and
+   * instead to re-display the form to the user with the corresponding elements
+   * rendered with an 'error' CSS class (shown as red by default).
+   *
+   * The standard behavior of this method can be changed if a button provides
+   * the #limit_validation_errors property. Multistep forms not wanting to
+   * validate the whole form can set #limit_validation_errors on buttons to
+   * limit validation errors to only certain elements. For example, pressing the
+   * "Previous" button in a multistep form should not fire validation errors
+   * just because the current step has invalid values. If
+   * #limit_validation_errors is set on a clicked button, the button must also
+   * define a #submit property (may be set to an empty array). Any #submit
+   * handlers will be executed even if there is invalid input, so extreme care
+   * should be taken with respect to any actions taken 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).
+   *
+   * Example 1: Allow the "Previous" button to function, regardless of whether
+   * any user input is valid.
+   *
+   * @code
+   *   $form['actions']['previous'] = array(
+   *     '#type' => 'submit',
+   *     '#value' => t('Previous'),
+   *     '#limit_validation_errors' => array(),       // No validation.
+   *     '#submit' => array('some_submit_function'),  // #submit required.
+   *   );
+   * @endcode
+   *
+   * Example 2: Require some, but not all, user input to be valid to process the
+   * submission of a "Previous" button.
+   *
+   * @code
+   *   $form['actions']['previous'] = array(
+   *     '#type' => 'submit',
+   *     '#value' => t('Previous'),
+   *     '#limit_validation_errors' => array(
+   *       array('step1'),      // Validate $form_state['values']['step1'].
+   *       array('foo', 'bar'), // Validate $form_state['values']['foo']['bar'].
+   *     ),
+   *     '#submit' => array('some_submit_function'), // #submit required.
+   *   );
+   * @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 FormErrorInterface::setErrorByName('step1', $form_state, $message)
+   * or
+   * FormErrorInterface::setErrorByName('step1][choice', $form_state, $message)
+   * will prevent the submit handlers from running, and result in the error
+   * message being displayed to the user. However, calls to
+   * FormErrorInterface::setErrorByName('step2', $form_state, $message) and
+   * FormErrorInterface::setErrorByName('step2][groupX][choiceY', $form_state, $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 flagging invalid values for
+   * $form_state['values']['foo']['bar'] and everything within it will be
+   * flagged and submission prevented.
+   *
+   * 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.
+   *
+   * @param string $name
+   *   The name of the form element. If the #parents property of your form
+   *   element is array('foo', 'bar', 'baz') then you may set an error on 'foo'
+   *   or 'foo][bar][baz'. Setting an error on 'foo' sets an error for every
+   *   element where the #parents array starts with 'foo'.
+   * @param string $message
+   *   (optional) The error message to present to the user.
+   *
+   * @return $this
+   */
+  public function setErrorByName($name, $message = '');
+
+  /**
+   * Flags an element as having an error.
+   *
+   * @param array $element
+   *   The form element.
+   * @param string $message
+   *   (optional) The error message to present to the user.
+   *
+   * @return $this
+   */
+  public function setError(&$element, $message = '');
+
+  /**
+   * Clears all errors against all form elements made by FormErrorInterface::setErrorByName().
+   */
+  public function clearErrors();
+
+  /**
+   * Returns an associative array of all errors.
+   *
+   * @return array
+   *   An array of all errors, keyed by the name of the form element.
+   */
+  public function getErrors();
+
+  /**
+   * Returns the error message filed against the given form element.
+   *
+   * Form errors higher up in the form structure override deeper errors as well
+   * as errors on the element itself.
+   *
+   * @param array $element
+   *   The form element to check for errors.
+   *
+   * @return string|null
+   *   Either the error message for this element or NULL if there are no errors.
+   */
+  public function getError($element);
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormSubmitter.php b/core/lib/Drupal/Core/Form/FormSubmitter.php
index 78aca72..616cd8d 100644
--- a/core/lib/Drupal/Core/Form/FormSubmitter.php
+++ b/core/lib/Drupal/Core/Form/FormSubmitter.php
@@ -61,18 +61,7 @@ public function doSubmitForm(&$form, &$form_state) {
     // \Drupal\Core\Form\FormBuilderInterface::submitForm).
     if ($batch = &$this->batchGet() && !isset($batch['current_set'])) {
       // Store $form_state information in the batch definition.
-      // We need the full $form_state when either:
-      // - Some submit handlers were saved to be called during batch
-      //   processing. See self::executeSubmitHandlers().
-      // - The form is multistep.
-      // In other cases, we only need the information expected by
-      // self::redirectForm().
-      if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) {
-        $batch['form_state'] = $form_state;
-      }
-      else {
-        $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect', 'redirect_route')));
-      }
+      $batch['form_state'] = $form_state;
 
       $batch['progressive'] = !$form_state['programmed'];
       $response = batch_process();
@@ -103,13 +92,13 @@ public function doSubmitForm(&$form, &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function executeSubmitHandlers(&$form, &$form_state) {
+  public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) {
     // If there was a button pressed, use its handlers.
-    if (isset($form_state['submit_handlers'])) {
+    if (!empty($form_state['submit_handlers'])) {
       $handlers = $form_state['submit_handlers'];
     }
     // Otherwise, check for a form-level handler.
-    elseif (isset($form['#submit'])) {
+    elseif (!empty($form['#submit'])) {
       $handlers = $form['#submit'];
     }
     else {
@@ -137,76 +126,53 @@ public function executeSubmitHandlers(&$form, &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function redirectForm($form_state) {
-    // Skip redirection for form submissions invoked via
-    // \Drupal\Core\Form\FormBuilderInterface::submitForm().
-    if (!empty($form_state['programmed'])) {
-      return;
-    }
-    // Skip redirection if rebuild is activated.
-    if (!empty($form_state['rebuild'])) {
-      return;
-    }
-    // Skip redirection if it was explicitly disallowed.
-    if (!empty($form_state['no_redirect'])) {
-      return;
-    }
+  public function redirectForm(FormStateInterface $form_state) {
+    // According to RFC 7231, 303 See Other status code must be used to redirect
+    // user agent (and not default 302 Found).
+    // @see http://tools.ietf.org/html/rfc7231#section-6.4.4
+    $status_code = Response::HTTP_SEE_OTHER;
+    $redirect = $form_state->getRedirect();
 
     // Allow using redirect responses directly if needed.
-    if (isset($form_state['redirect']) && $form_state['redirect'] instanceof RedirectResponse) {
-      return $form_state['redirect'];
+    if ($redirect instanceof RedirectResponse) {
+      return $redirect;
     }
 
+    $url = NULL;
     // Check for a route-based redirection.
-    if (isset($form_state['redirect_route'])) {
-      // @todo Remove once all redirects are converted to Url.
-      if (!($form_state['redirect_route'] instanceof Url)) {
-        $form_state['redirect_route'] += array(
-          'route_parameters' => array(),
-          'options' => array(),
-        );
-        $form_state['redirect_route'] = new Url($form_state['redirect_route']['route_name'], $form_state['redirect_route']['route_parameters'], $form_state['redirect_route']['options']);
-      }
-
-      $form_state['redirect_route']->setAbsolute();
-      // According to RFC 7231, 303 See Other status code must be used
-      // to redirect user agent (and not default 302 Found).
-      // @see http://tools.ietf.org/html/rfc7231#section-6.4.4
-      return new RedirectResponse($form_state['redirect_route']->toString(), Response::HTTP_SEE_OTHER);
+    if ($redirect instanceof Url) {
+      $url = $redirect->toString();
     }
-
-    // Only invoke a redirection if redirect value was not set to FALSE.
-    if (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE) {
-      if (isset($form_state['redirect'])) {
-        if (is_array($form_state['redirect'])) {
-          if (isset($form_state['redirect'][1])) {
-            $options = $form_state['redirect'][1];
-          }
-          else {
-            $options = array();
-          }
-          // Redirections should always use absolute URLs.
-          $options['absolute'] = TRUE;
-          if (isset($form_state['redirect'][2])) {
-            $status_code = $form_state['redirect'][2];
-          }
-          else {
-            $status_code = Response::HTTP_SEE_OTHER;
-          }
-          return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'][0], $options), $status_code);
-        }
-        else {
-          // This function can be called from the installer, which guarantees
-          // that $redirect will always be a string, so catch that case here
-          // and use the appropriate redirect function.
-          if ($this->drupalInstallationAttempted()) {
-            install_goto($form_state['redirect']);
-          }
-          else {
-            return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'], array('absolute' => TRUE)), Response::HTTP_SEE_OTHER);
-          }
-        }
+    // An array contains the path to use for the redirect, as well as options to
+    // use for generating the URL.
+    elseif (is_array($redirect)) {
+      if (isset($redirect[1])) {
+        $options = $redirect[1];
       }
+      else {
+        $options = array();
+      }
+      // Redirections should always use absolute URLs.
+      $options['absolute'] = TRUE;
+      if (isset($redirect[2])) {
+        $status_code = $redirect[2];
+      }
+      $url = $this->urlGenerator->generateFromPath($redirect[0], $options);
+    }
+    // A string represents the path to use for the redirect.
+    elseif (is_string($redirect)) {
+      // This function can be called from the installer, which guarantees
+      // that $redirect will always be a string, so catch that case here
+      // and use the appropriate redirect function.
+      if ($this->drupalInstallationAttempted()) {
+        install_goto($redirect);
+      }
+      else {
+        $url = $this->urlGenerator->generateFromPath($redirect, array('absolute' => TRUE));
+      }
+    }
+    // If no redirect was specified, redirect to the current path.
+    elseif ($redirect === NULL) {
       $request = $this->requestStack->getCurrentRequest();
       // @todo Remove dependency on the internal _system_path attribute:
       //   https://www.drupal.org/node/2293521.
@@ -214,7 +180,10 @@ public function redirectForm($form_state) {
         'query' => $request->query->all(),
         'absolute' => TRUE,
       ));
-      return new RedirectResponse($url, Response::HTTP_SEE_OTHER);
+    }
+
+    if ($url) {
+      return new RedirectResponse($url, $status_code);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php
index eda8238..d0a57d9 100644
--- a/core/lib/Drupal/Core/Form/FormValidator.php
+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -412,91 +412,43 @@ protected function setElementErrorsFromFormState(array &$elements, array &$form_
   /**
    * {@inheritdoc}
    */
-  public function setErrorByName($name, array &$form_state, $message = '') {
-    if (!empty($form_state['validation_complete'])) {
-      throw new \LogicException('Form errors cannot be set after form validation has finished.');
-    }
-
-    if (!isset($form_state['errors'][$name])) {
-      $record = TRUE;
-      if (isset($form_state['limit_validation_errors'])) {
-        // #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 ($form_state['limit_validation_errors'] 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. As the exploded array
-          // will all be strings, we need to cast every value of the section
-          // array to string.
-          if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
-            $record = TRUE;
-            break;
-          }
-        }
-      }
-      if ($record) {
-        $form_state['errors'][$name] = $message;
-        $this->requestStack->getCurrentRequest()->attributes->set('_form_errors', TRUE);
-        if ($message) {
-          $this->drupalSetMessage($message, 'error');
-        }
-      }
-    }
-
-    return $form_state['errors'];
+  public function setErrorByName($name, FormStateInterface &$form_state, $message = '') {
+    return $form_state->setErrorByName($name, $message);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setError(&$element, array &$form_state, $message = '') {
-    $this->setErrorByName(implode('][', $element['#parents']), $form_state, $message);
+  public function setError(&$element, FormStateInterface &$form_state, $message = '') {
+    return $form_state->setError($element, $message);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getError($element, array &$form_state) {
-    if ($errors = $this->getErrors($form_state)) {
-      $parents = array();
-      foreach ($element['#parents'] as $parent) {
-        $parents[] = $parent;
-        $key = implode('][', $parents);
-        if (isset($errors[$key])) {
-          return $errors[$key];
-        }
-      }
-    }
+  public function getError($element, FormStateInterface &$form_state) {
+    return $form_state->getError($element);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function clearErrors(array &$form_state) {
-    $form_state['errors'] = array();
-    $this->requestStack->getCurrentRequest()->attributes->set('_form_errors', FALSE);
+  public function clearErrors(FormStateInterface &$form_state) {
+    $form_state->clearErrors();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getErrors(array $form_state) {
-    return $form_state['errors'];
+  public function getErrors(FormStateInterface &$form_state) {
+    return $form_state->getErrors();
   }
 
   /**
    * {@inheritdoc}
    */
   public function getAnyErrors() {
-    return (bool) $this->requestStack->getCurrentRequest()->attributes->get('_form_errors');
+    return FormState::hasAnyErrors();
   }
 
   /**
@@ -506,13 +458,4 @@ protected function watchdog($type, $message, array $variables = array(), $severi
     watchdog($type, $message, $variables, $severity, $link);
   }
 
-  /**
-   * Wraps drupal_set_message().
-   *
-   * @return array|null
-   */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-    return drupal_set_message($message, $type, $repeat);
-  }
-
 }
diff --git a/core/modules/block/src/BlockBase.php b/core/modules/block/src/BlockBase.php
index e87f4a7..b609158 100644
--- a/core/modules/block/src/BlockBase.php
+++ b/core/modules/block/src/BlockBase.php
@@ -345,10 +345,12 @@ public function validateConfigurationForm(array &$form, array &$form_state) {
 
     foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
       // Allow the condition to validate the form.
-      $condition_values = array(
-        'values' => &$form_state['values']['visibility'][$condition_id],
-      );
+      $condition_values = new FormState(array(
+        'values' => $form_state['values']['visibility'][$condition_id],
+      ));
       $condition->validateConfigurationForm($form, $condition_values);
+      // Update the original form values.
+      $form_state['values']['visibility'][$condition_id] = $condition_values['values'];
     }
 
     $this->blockValidate($form, $form_state);
@@ -376,10 +378,12 @@ public function submitConfigurationForm(array &$form, array &$form_state) {
       $this->configuration['cache'] = $form_state['values']['cache'];
       foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
         // Allow the condition to submit the form.
-        $condition_values = array(
-          'values' => &$form_state['values']['visibility'][$condition_id],
-        );
+        $condition_values = new FormState(array(
+          'values' => $form_state['values']['visibility'][$condition_id],
+        ));
         $condition->submitConfigurationForm($form, $condition_values);
+        // Update the original form values.
+        $form_state['values']['visibility'][$condition_id] = $condition_values['values'];
       }
       $this->blockSubmit($form, $form_state);
     }
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index f562f52..63f239d 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -147,11 +147,13 @@ public function validate(array $form, array &$form_state) {
 
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for validation.
-    $settings = array(
-      'values' => &$form_state['values']['settings']
-    );
+    $settings = new FormState(array(
+      'values' => $form_state['values']['settings']
+    ));
     // Call the plugin validate handler.
     $this->entity->getPlugin()->validateConfigurationForm($form, $settings);
+    // Update the original form values.
+    $form_state['values']['settings'] = $settings['values'];
   }
 
   /**
@@ -164,13 +166,14 @@ public function submit(array $form, array &$form_state) {
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for submission.
     // @todo Find a way to avoid this manipulation.
-    $settings = array(
-      'values' => &$form_state['values']['settings'],
-      'errors' => $form_state['errors'],
-    );
+    $settings = new FormState(array(
+      'values' => $form_state['values']['settings'],
+    ));
 
     // Call the plugin submit handler.
     $entity->getPlugin()->submitConfigurationForm($form, $settings);
+    // Update the original form values.
+    $form_state['values']['settings'] = $settings['values'];
 
     // Save the settings of the plugin.
     $entity->save();
diff --git a/core/modules/block/src/Tests/BlockInterfaceTest.php b/core/modules/block/src/Tests/BlockInterfaceTest.php
index 77cdc7a..ad5dcb8 100644
--- a/core/modules/block/src/Tests/BlockInterfaceTest.php
+++ b/core/modules/block/src/Tests/BlockInterfaceTest.php
@@ -118,7 +118,7 @@ public function testBlockInterface() {
         '#default_value' => 'My custom display message.',
       ),
     );
-    $form_state = array();
+    $form_state = new FormState();
     // Ensure there are no form elements that do not belong to the plugin.
     $actual_form = $display_block->buildConfigurationForm(array(), $form_state);
     // Remove the visibility sections, as that just tests condition plugins.
diff --git a/core/modules/config/src/Form/ConfigSingleExportForm.php b/core/modules/config/src/Form/ConfigSingleExportForm.php
index 14255b5..fe694b5 100644
--- a/core/modules/config/src/Form/ConfigSingleExportForm.php
+++ b/core/modules/config/src/Form/ConfigSingleExportForm.php
@@ -121,10 +121,10 @@ public function buildForm(array $form, array &$form_state, $config_type = NULL,
       '#suffix' => '</div>',
     );
     if ($config_type && $config_name) {
-      $fake_form_state = array('values' => array(
+      $fake_form_state = new FormState(array('values' => array(
         'config_type' => $config_type,
         'config_name' => $config_name,
-      ));
+      )));
       $form['export'] = $this->updateExport($form, $fake_form_state);
     }
     return $form;
diff --git a/core/modules/field/src/Tests/FieldAttachOtherTest.php b/core/modules/field/src/Tests/FieldAttachOtherTest.php
index 7356bce..386cb67 100644
--- a/core/modules/field/src/Tests/FieldAttachOtherTest.php
+++ b/core/modules/field/src/Tests/FieldAttachOtherTest.php
@@ -265,7 +265,7 @@ function testEntityFormDisplayBuildForm() {
     // Test generating widgets for all fields.
     $display = entity_get_form_display($entity_type, $this->instance->bundle, 'default');
     $form = array();
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
     $display->buildForm($entity, $form, $form_state);
 
     $this->assertEqual($form[$this->field_name]['widget']['#title'], $this->instance->getLabel(), "First field's form title is {$this->instance->getLabel()}");
@@ -287,7 +287,7 @@ function testEntityFormDisplayBuildForm() {
       }
     }
     $form = array();
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
     $display->buildForm($entity, $form, $form_state);
 
     $this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form');
@@ -310,7 +310,7 @@ function testEntityFormDisplayExtractFormValues() {
     // Build the form for all fields.
     $display = entity_get_form_display($entity_type, $this->instance->bundle, 'default');
     $form = array();
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
     $display->buildForm($entity_init, $form, $form_state);
 
     // Simulate incoming values.
diff --git a/core/modules/field/src/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php
index 2457aa7..3de95d9 100644
--- a/core/modules/field/src/Tests/FormTest.php
+++ b/core/modules/field/src/Tests/FormTest.php
@@ -531,7 +531,7 @@ function testFieldFormAccess() {
 
     $display = entity_get_form_display($entity_type, $entity_type, 'default');
     $form = array();
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
     $display->buildForm($entity, $form, $form_state);
 
     $this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.');
diff --git a/core/modules/image/src/Form/ImageEffectFormBase.php b/core/modules/image/src/Form/ImageEffectFormBase.php
index 6cc8d1d..b20304d 100644
--- a/core/modules/image/src/Form/ImageEffectFormBase.php
+++ b/core/modules/image/src/Form/ImageEffectFormBase.php
@@ -104,10 +104,12 @@ public function buildForm(array $form, array &$form_state, ImageStyleInterface $
   public function validateForm(array &$form, array &$form_state) {
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for validation.
-    $effect_data = array(
-      'values' => &$form_state['values']['data']
-    );
+    $effect_data = new FormState(array(
+      'values' => $form_state['values']['data'],
+    ));
     $this->imageEffect->validateConfigurationForm($form, $effect_data);
+    // Update the original form values.
+    $form_state['values']['data'] = $effect_data['values'];
   }
 
   /**
@@ -118,10 +120,13 @@ public function submitForm(array &$form, array &$form_state) {
 
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for submission.
-    $effect_data = array(
-      'values' => &$form_state['values']['data']
-    );
+    $effect_data = new FormState(array(
+      'values' => $form_state['values']['data'],
+    ));
     $this->imageEffect->submitConfigurationForm($form, $effect_data);
+    // Update the original form values.
+    $form_state['values']['data'] = $effect_data['values'];
+
     $this->imageEffect->setWeight($form_state['values']['weight']);
     if (!$this->imageEffect->getUuid()) {
       $this->imageStyle->addImageEffect($this->imageEffect->getConfiguration());
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index d854f3c..0173414 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -249,7 +249,7 @@ protected function buildOverviewForm(array &$form, array &$form_state) {
     // section.
     $form['#tree'] = TRUE;
     $form['#theme'] = 'menu_overview_form';
-    $form_state += array('menu_overview_form_parents' => array());
+    $form_state->setIfNotExists('menu_overview_form_parents', array());
 
     $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css');
 
diff --git a/core/modules/quickedit/src/QuickEditController.php b/core/modules/quickedit/src/QuickEditController.php
index 2c1c093..1586a53 100644
--- a/core/modules/quickedit/src/QuickEditController.php
+++ b/core/modules/quickedit/src/QuickEditController.php
@@ -177,13 +177,13 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
       $this->tempStoreFactory->get('quickedit')->set($entity->uuid(), $entity);
     }
 
-    $form_state = array(
+    $form_state = new FormState(array(
       'langcode' => $langcode,
       'no_redirect' => TRUE,
       'build_info' => array(
         'args' => array($entity, $field_name),
       ),
-    );
+    ));
     $form = $this->formBuilder()->buildForm('Drupal\quickedit\Form\QuickEditFieldForm', $form_state);
 
     if (!empty($form_state['executed'])) {
diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index e2b4960..39143ac 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -269,7 +269,7 @@ public function submitForm(array &$form, array &$form_state) {
     }
 
     $form_execute = array();
-    $form_state_execute = array('values' => array());
+    $form_state_execute = new FormState(array('values' => array()));
     foreach ($classes as $class) {
       $form_state_execute['values']['tests'][$class] = $class;
     }
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
index bc43c49..4ee5b73 100644
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ b/core/modules/system/src/Controller/FormAjaxController.php
@@ -70,7 +70,7 @@ public function content(Request $request) {
    * @throws Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
    */
   protected function getForm(Request $request) {
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
     $form_build_id = $request->request->get('form_build_id');
 
     // Get the form from the cache.
diff --git a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php
index b0ce2f8..45c21b2 100644
--- a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php
+++ b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php
@@ -207,7 +207,7 @@ function testMultipleFalseOptionchecker() {
    */
   private function formSubmitHelper($form, $edit) {
     $form_id = $this->randomName();
-    $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $form_state = new FormState();
 
     $form['op'] = array('#type' => 'submit', '#value' => t('Submit'));
     // The form token CSRF protection should not interfere with this test, so we
diff --git a/core/modules/system/src/Tests/Form/FormCacheTest.php b/core/modules/system/src/Tests/Form/FormCacheTest.php
index b33c749..357002a 100644
--- a/core/modules/system/src/Tests/Form/FormCacheTest.php
+++ b/core/modules/system/src/Tests/Form/FormCacheTest.php
@@ -32,7 +32,7 @@ public function setUp() {
     $this->form = array(
       '#property' => $this->randomName(),
     );
-    $this->form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $this->form_state = new FormState();
     $this->form_state['example'] = $this->randomName();
   }
 
@@ -43,7 +43,7 @@ function testCacheToken() {
     \Drupal::currentUser()->setAccount(new UserSession(array('uid' => 1)));
     form_set_cache($this->form_build_id, $this->form, $this->form_state);
 
-    $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $cached_form_state = new FormState();
     $cached_form = form_get_cache($this->form_build_id, $cached_form_state);
     $this->assertEqual($this->form['#property'], $cached_form['#property']);
     $this->assertTrue(!empty($cached_form['#cache_token']), 'Form has a cache token');
@@ -53,14 +53,14 @@ function testCacheToken() {
     // Change the private key. (We cannot change the session ID because this
     // will break the parent site test runner batch.)
     \Drupal::state()->set('system.private_key', 'invalid');
-    $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $cached_form_state = new FormState();
     $cached_form = form_get_cache($this->form_build_id, $cached_form_state);
     $this->assertFalse($cached_form, 'No form returned from cache');
     $this->assertTrue(empty($cached_form_state['example']));
 
     // Test that loading the cache with a different form_id fails.
     $wrong_form_build_id = $this->randomName(9);
-    $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $cached_form_state = new FormState();
     $this->assertFalse(form_get_cache($wrong_form_build_id, $cached_form_state), 'No form returned from cache');
     $this->assertTrue(empty($cached_form_state['example']), 'Cached form state was not loaded');
   }
@@ -74,7 +74,7 @@ function testNoCacheToken() {
     $this->form_state['example'] = $this->randomName();
     form_set_cache($this->form_build_id, $this->form, $this->form_state);
 
-    $cached_form_state = \Drupal::formBuilder()->getFormStateDefaults();
+    $cached_form_state = new FormState();
     $cached_form = form_get_cache($this->form_build_id, $cached_form_state);
     $this->assertEqual($this->form['#property'], $cached_form['#property']);
     $this->assertTrue(empty($cached_form['#cache_token']), 'Form has no cache token');
diff --git a/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php b/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
index 9c980a6..c1c29cb 100644
--- a/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
+++ b/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
@@ -73,7 +73,7 @@ public function submitForm(array &$form, array &$form_state) {
    * Tests that default handlers are added even if custom are specified.
    */
   function testDefaultAndCustomHandlers() {
-    $form_state['values'] = array();
+    $form_state = new FormState(array('values' => array()));
     $form_builder = $this->container->get('form_builder');
     $form_builder->submitForm($this, $form_state);
 
diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php
index 2b4850e..b53e8fc 100644
--- a/core/modules/system/src/Tests/Form/FormTest.php
+++ b/core/modules/system/src/Tests/Form/FormTest.php
@@ -102,7 +102,7 @@ function testRequiredFields() {
         foreach (array(TRUE, FALSE) as $required) {
           $form_id = $this->randomName();
           $form = array();
-          $form_state = \Drupal::formBuilder()->getFormStateDefaults();
+          $form_state = new FormState();
           $form['op'] = array('#type' => 'submit', '#value' => t('Submit'));
           $element = $data['element']['#title'];
           $form[$element] = $data['element'];
@@ -482,7 +482,7 @@ function testColorValidation() {
    */
   function testDisabledElements() {
     // Get the raw form in its original state.
-    $form_state = array();
+    $form_state = new FormState();
     $form = (new FormTestDisabledElementsForm())->buildForm(array(), $form_state);
 
     // Build a submission that tries to hijack the form by submitting input for
diff --git a/core/modules/system/src/Tests/Form/ProgrammaticTest.php b/core/modules/system/src/Tests/Form/ProgrammaticTest.php
index a5c4071..2d15c27 100644
--- a/core/modules/system/src/Tests/Form/ProgrammaticTest.php
+++ b/core/modules/system/src/Tests/Form/ProgrammaticTest.php
@@ -71,7 +71,7 @@ function testSubmissionWorkflow() {
    */
   private function submitForm($values, $valid_input) {
     // Programmatically submit the given values.
-    $form_state = array('values' => $values);
+    $form_state = new FormState(array('values' => $values));
     \Drupal::formBuilder()->submitForm('\Drupal\form_test\Form\FormTestProgrammaticForm', $form_state);
 
     // Check that the form returns an error when expected, and vice versa.
@@ -98,6 +98,7 @@ private function submitForm($values, $valid_input) {
    * Test the programmed_bypass_access_check flag.
    */
   public function testProgrammaticAccessBypass() {
+    $form_state = new FormState();
     $form_state['values'] = array(
       'textfield' => 'dummy value',
       'field_restricted' => 'dummy value'
diff --git a/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php b/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php
index 3e6f413..89d7a38 100644
--- a/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php
+++ b/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php
@@ -73,6 +73,7 @@ public function submitForm(array &$form, array &$form_state) {
    */
   function testLimitValidationErrors() {
     // Programmatically submit the form.
+    $form_state = new FormState();
     $form_state['values'] = array();
     $form_state['values']['section'] = 'one';
     $form_builder = $this->container->get('form_builder');
diff --git a/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php b/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
index b923daf..a0852e7 100644
--- a/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
+++ b/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
@@ -47,11 +47,12 @@
    */
   public function testConfigForm() {
     // Programmatically submit the given values.
+    $values = array();
     foreach ($this->values as $form_key => $data) {
       $values[$form_key] = $data['#value'];
     }
-    $form_state = array('values' => $values);
-    drupal_form_submit($this->form, $form_state);
+    $form_state = new FormState(array('values' => $values));
+    \Drupal::formBuilder()->submitForm($this->form, $form_state);
 
     // Check that the form returns an error when expected, and vice versa.
     $errors = form_get_errors($form_state);
diff --git a/core/modules/system/tests/modules/batch_test/batch_test.module b/core/modules/system/tests/modules/batch_test/batch_test.module
index 5438acf..6deb8b8 100644
--- a/core/modules/system/tests/modules/batch_test/batch_test.module
+++ b/core/modules/system/tests/modules/batch_test/batch_test.module
@@ -9,6 +9,7 @@
  * Batch operation: Submits form_test_mock_form() using drupal_form_submit().
  */
 function _batch_test_nested_drupal_form_submit_callback($value) {
+  $state = new FormState();
   $state['values']['test_value'] = $value;
   \Drupal::formBuilder()->submitForm('Drupal\batch_test\Form\BatchTestMockForm', $state);
 }
diff --git a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
index 9afb0e9..25b4e12 100644
--- a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
+++ b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
@@ -84,9 +84,9 @@ public function testNoForm() {
    *   Render array containing markup.
    */
   function testProgrammatic($value = 1) {
-    $form_state = array(
+    $form_state = new FormState(array(
       'values' => array('value' => $value)
-    );
+    ));
     \Drupal::formBuilder()->submitForm('Drupal\batch_test\Form\BatchTestChainedForm', $form_state);
     return array(
       'success' => array(
diff --git a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
index b560a0c..5b0281a 100644
--- a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
+++ b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
@@ -30,7 +30,7 @@ class UserAccountFormFieldsTest extends DrupalUnitTestBase {
   function testInstallConfigureForm() {
     require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
     $install_state = install_state_defaults();
-    $form_state = array();
+    $form_state = new FormState();
     $form_state['build_info']['args'][] = &$install_state;
     $form = $this->container->get('form_builder')
       ->buildForm('Drupal\Core\Installer\Form\SiteConfigureForm', $form_state);
diff --git a/core/modules/user/src/Tests/Views/ArgumentValidateTest.php b/core/modules/user/src/Tests/Views/ArgumentValidateTest.php
index f1e9c0e..f9fa5f1 100644
--- a/core/modules/user/src/Tests/Views/ArgumentValidateTest.php
+++ b/core/modules/user/src/Tests/Views/ArgumentValidateTest.php
@@ -45,7 +45,8 @@ function testArgumentValidateUserUid() {
     // Fail for a valid numeric, but for a user that doesn't exist
     $this->assertFalse($view->argument['null']->validateArgument(32));
 
-    $form = $form_state = array();
+    $form = array();
+    $form_state = new FormState();
     $view->argument['null']->buildOptionsForm($form, $form_state);
     $sanitized_id = ArgumentPluginBase::encodeValidatorId('entity:user');
     $this->assertTrue($form['validate']['options'][$sanitized_id]['roles']['#states']['visible'][':input[name="options[validate][options][' . $sanitized_id . '][restrict_roles]"]']['checked']);
diff --git a/core/modules/views/includes/ajax.inc b/core/modules/views/includes/ajax.inc
index 84c33eb..bdca807 100644
--- a/core/modules/views/includes/ajax.inc
+++ b/core/modules/views/includes/ajax.inc
@@ -14,16 +14,14 @@
  * some AJAX stuff automatically.
  * This makes some assumptions about the client.
  */
-function views_ajax_form_wrapper($form_class, &$form_state) {
+function views_ajax_form_wrapper($form_class, FormStateInterface &$form_state) {
   // This won't override settings already in.
-  $form_state += array(
-    'rerender' => FALSE,
-    'no_redirect' => !empty($form_state['ajax']),
-    'no_cache' => TRUE,
-    'build_info' => array(
-      'args' => array(),
-    ),
-  );
+  $form_state->setIfNotExists('rerender', FALSE);
+  $form_state->setIfNotExists('no_redirect', !empty($form_state['ajax']));
+  $form_state->setIfNotExists('no_cache', TRUE);
+  $form_state->setIfNotExists('build_info', array(
+    'args' => array(),
+  ));
 
   $form = \Drupal::formBuilder()->buildForm($form_class, $form_state);
   $output = drupal_render($form);
diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
index 8d98744..fdaae5b 100644
--- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
+++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
@@ -127,14 +127,14 @@ public function buildOptionsForm(&$form, &$form_state) {
    */
   public function renderExposedForm($block = FALSE) {
     // Deal with any exposed filters we may have, before building.
-    $form_state = array(
+    $form_state = new FormState(array(
       'view' => &$this->view,
       'display' => &$this->view->display_handler->display,
       'method' => 'get',
       'rerender' => TRUE,
       'no_redirect' => TRUE,
       'always_process' => TRUE,
-    );
+    ));
 
     // Some types of displays (eg. attachments) may wish to use the exposed
     // filters of their parent displays instead of showing an additional
diff --git a/core/modules/views/src/Tests/Handler/AreaEntityTest.php b/core/modules/views/src/Tests/Handler/AreaEntityTest.php
index ac62de1..d609485 100644
--- a/core/modules/views/src/Tests/Handler/AreaEntityTest.php
+++ b/core/modules/views/src/Tests/Handler/AreaEntityTest.php
@@ -121,7 +121,7 @@ public function testEntityArea() {
 
     // Test the available view mode options.
     $form = array();
-    $form_state = array();
+    $form_state = new FormState();
     $form_state['type'] = 'header';
     $view->display_handler->getHandler('header', 'entity_entity_test')->buildOptionsForm($form, $form_state);
     $this->assertTrue(isset($form['view_mode']['#options']['test']), 'Ensure that the test view mode is available.');
diff --git a/core/modules/views/src/Tests/Plugin/RowEntityTest.php b/core/modules/views/src/Tests/Plugin/RowEntityTest.php
index e422d96..9dce6c7 100644
--- a/core/modules/views/src/Tests/Plugin/RowEntityTest.php
+++ b/core/modules/views/src/Tests/Plugin/RowEntityTest.php
@@ -59,7 +59,7 @@ public function testEntityRow() {
 
     // Tests the available view mode options.
     $form = array();
-    $form_state = array();
+    $form_state = new FormState();
     $form_state['view'] = $view->storage;
     $view->rowPlugin->buildOptionsForm($form, $form_state);
 
diff --git a/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php b/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
index 37880d4..58cf77a 100644
--- a/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
+++ b/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
@@ -50,7 +50,7 @@ protected function setUp() {
    */
   public function testCreateView() {
     $form = array();
-    $form_state = array();
+    $form_state = new FormState();
     $form = $this->wizard->buildForm($form, $form_state);
     $random_id = strtolower($this->randomName());
     $random_label = $this->randomName();
diff --git a/core/modules/views_ui/src/Form/Ajax/Display.php b/core/modules/views_ui/src/Form/Ajax/Display.php
index c2dbad6..c065655 100644
--- a/core/modules/views_ui/src/Form/Ajax/Display.php
+++ b/core/modules/views_ui/src/Form/Ajax/Display.php
@@ -35,9 +35,9 @@ public function getFormKey() {
    *   $form_state['type'].
    */
   public function getFormState(ViewStorageInterface $view, $display_id, $js) {
-    return array(
-      'section' => $this->type,
-    ) + parent::getFormState($view, $display_id, $js);
+    $form_state = parent::getFormState($view, $display_id, $js);
+    $form_state['section'] = $this->type;
+    return $form_state;
   }
 
   /**
diff --git a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
index f7d4b86..4c12fab 100644
--- a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -63,7 +63,7 @@ protected function setType($type) {
   public function getFormState(ViewStorageInterface $view, $display_id, $js) {
     // $js may already have been converted to a Boolean.
     $ajax = is_string($js) ? $js === 'ajax' : $js;
-    return array(
+    return new FormState(array(
       'form_id' => $this->getFormId(),
       'form_key' => $this->getFormKey(),
       'ajax' => $ajax,
@@ -76,7 +76,7 @@ public function getFormState(ViewStorageInterface $view, $display_id, $js) {
         'args' => array(),
         'callback_object' => $this,
       ),
-    );
+    ));
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php b/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php
index add47b1..4ec28d2 100644
--- a/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php
+++ b/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Display;
 
+use Drupal\Core\Form\FormState;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -142,6 +143,7 @@ public function testSubmitConfigurationForm() {
 
     $form = array();
     $label = $this->randomName();
+    $form_state = new FormState();
     $form_state['values']['label'] = $label;
     $display_variant->submitConfigurationForm($form, $form_state);
     $this->assertSame($label, $display_variant->label());
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityFormBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityFormBuilderTest.php
index 14622d2..2e7ab80 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityFormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityFormBuilderTest.php
@@ -60,7 +60,7 @@ public function testGetForm() {
 
     $this->formBuilder->expects($this->once())
       ->method('buildForm')
-      ->with($form_controller, $this->isType('array'))
+      ->with($form_controller, $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'))
       ->will($this->returnValue('the form contents'));
 
     $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 4702b9d..e867106 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -9,6 +9,8 @@
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -26,11 +28,12 @@ class FormBuilderTest extends FormTestBase {
   public function testGetFormIdWithString() {
     $form_arg = 'foo';
 
-    $form_state = array();
+    $clean_form_state = new FormState();
+    $form_state = new FormState();
     $form_id = $this->formBuilder->getFormId($form_arg, $form_state);
 
     $this->assertSame($form_arg, $form_id);
-    $this->assertEmpty($form_state);
+    $this->assertSame($clean_form_state, $form_state);
   }
 
   /**
@@ -39,7 +42,7 @@ public function testGetFormIdWithString() {
   public function testGetFormIdWithClassName() {
     $form_arg = 'Drupal\Tests\Core\Form\TestForm';
 
-    $form_state = array();
+    $form_state = new FormState();
     $form_id = $this->formBuilder->getFormId($form_arg, $form_state);
 
     $this->assertSame('test_form', $form_id);
@@ -55,7 +58,7 @@ public function testGetFormIdWithInjectedClassName() {
 
     $form_arg = 'Drupal\Tests\Core\Form\TestFormInjected';
 
-    $form_state = array();
+    $form_state = new FormState();
     $form_id = $this->formBuilder->getFormId($form_arg, $form_state);
 
     $this->assertSame('test_form', $form_id);
@@ -70,7 +73,7 @@ public function testGetFormIdWithObject() {
 
     $form_arg = $this->getMockForm($expected_form_id);
 
-    $form_state = array();
+    $form_state = new FormState();
     $form_id = $this->formBuilder->getFormId($form_arg, $form_state);
 
     $this->assertSame($expected_form_id, $form_id);
@@ -92,7 +95,7 @@ public function testGetFormIdWithBaseForm() {
       ->method('getBaseFormId')
       ->will($this->returnValue($base_form_id));
 
-    $form_state = array();
+    $form_state = new FormState();
     $form_id = $this->formBuilder->getFormId($form_arg, $form_state);
 
     $this->assertSame($expected_form_id, $form_id);
@@ -119,11 +122,11 @@ public function testHandleFormStateResponse($class, $form_state_key) {
     $form_arg = $this->getMockForm($form_id, $expected_form);
     $form_arg->expects($this->any())
       ->method('submitForm')
-      ->will($this->returnCallback(function ($form, &$form_state) use ($response, $form_state_key) {
+      ->will($this->returnCallback(function ($form, FormStateInterface $form_state) use ($response, $form_state_key) {
         $form_state[$form_state_key] = $response;
       }));
 
-    $form_state = array();
+    $form_state = new FormState();
     try {
       $form_state['values'] = array();
       $form_state['input']['form_id'] = $form_id;
@@ -171,13 +174,13 @@ public function testHandleRedirectWithResponse() {
     $form_arg = $this->getMockForm($form_id, $expected_form);
     $form_arg->expects($this->any())
       ->method('submitForm')
-      ->will($this->returnCallback(function ($form, &$form_state) use ($response, $redirect) {
+      ->will($this->returnCallback(function ($form, FormStateInterface $form_state) use ($response, $redirect) {
         // Set both the response and the redirect.
         $form_state['response'] = $response;
         $form_state['redirect'] = $redirect;
       }));
 
-    $form_state = array();
+    $form_state = new FormState();
     try {
       $form_state['values'] = array();
       $form_state['input']['form_id'] = $form_id;
@@ -226,7 +229,7 @@ public function testGetFormWithClassString() {
     $form_id = '\Drupal\Tests\Core\Form\TestForm';
     $object = new TestForm();
     $form = array();
-    $form_state = array();
+    $form_state = new FormState();
     $expected_form = $object->buildForm($form, $form_state);
 
     $form = $this->formBuilder->getForm($form_id);
@@ -256,7 +259,7 @@ public function testBuildFormWithClassString() {
     $form_id = '\Drupal\Tests\Core\Form\TestForm';
     $object = new TestForm();
     $form = array();
-    $form_state = array();
+    $form_state = new FormState();
     $expected_form = $object->buildForm($form, $form_state);
 
     $form = $this->formBuilder->buildForm($form_id, $form_state);
@@ -273,7 +276,7 @@ public function testBuildFormWithObject() {
 
     $form_arg = $this->getMockForm($form_id, $expected_form);
 
-    $form_state = array();
+    $form_state = new FormState();
     $form = $this->formBuilder->buildForm($form_arg, $form_state);
     $this->assertFormElement($expected_form, $form, 'test');
     $this->assertSame($form_id, $form_state['build_info']['form_id']);
@@ -297,7 +300,7 @@ public function testRebuildForm() {
       ->will($this->returnValue($expected_form));
 
     // Do an initial build of the form and track the build ID.
-    $form_state = array();
+    $form_state = new FormState();
     $form = $this->formBuilder->buildForm($form_arg, $form_state);
     $original_build_id = $form['#build_id'];
 
@@ -348,7 +351,7 @@ public function testGetCache() {
       ->will($this->returnValue(TRUE));
 
     // Do an initial build of the form and track the build ID.
-    $form_state = array();
+    $form_state = new FormState();
     $form_state['build_info']['args'] = array();
     $form_state['build_info']['files'] = array(array('module' => 'node', 'type' => 'pages.inc'));
     $form_state['cache'] = TRUE;
@@ -365,10 +368,11 @@ public function testGetCache() {
       ->will($this->returnValue($cached_form));
     $this->formStateCache->expects($this->once())
       ->method('get')
-      ->will($this->returnValue($form_state));
+      ->will($this->returnValue($form_state->getCacheableArray()));
 
     // The final form build will not trigger any actual form building, but will
     // use the form cache.
+    $form_state['executed'] = TRUE;
     $form_state['input']['form_id'] = $form_id;
     $form_state['input']['form_build_id'] = $form['#build_id'];
     $this->formBuilder->buildForm($form_arg, $form_state);
@@ -392,7 +396,7 @@ public function testSendResponse() {
     $form_arg = $this->getMockForm($form_id, $expected_form);
 
     // Do an initial build of the form and track the build ID.
-    $form_state = array();
+    $form_state = new FormState();
     $this->formBuilder->buildForm($form_arg, $form_state);
   }
 
@@ -413,11 +417,15 @@ public function testUniqueHtmlId() {
       ->method('buildForm')
       ->will($this->returnValue($expected_form));
 
-    $form_state = array();
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
     $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
     $this->assertSame($form_id, $form['#id']);
 
-    $form_state = array();
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
     $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
     $this->assertSame("$form_id--2", $form['#id']);
   }
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
new file mode 100644
index 0000000..3c60ddf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormStateTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormState
+ *
+ * @group Form
+ */
+class FormStateTest extends UnitTestCase {
+
+  /**
+   * Tests the getRedirect() method.
+   *
+   * @covers ::getRedirect
+   *
+   * @dataProvider providerTestGetRedirect
+   */
+  public function testGetRedirect($form_state_additions, $expected) {
+    $form_state = new FormState($form_state_additions);
+    $redirect = $form_state->getRedirect();
+    $this->assertEquals($expected, $redirect);
+  }
+
+  /**
+   * Provides test data for testing the getRedirect() method.
+   *
+   * @return array
+   *   Returns some test data.
+   */
+  public function providerTestGetRedirect() {
+    $data = array();
+    $data[] = array(array(), NULL);
+
+    $data[] = array(array('redirect' => 'foo'), 'foo');
+    $data[] = array(array('redirect' => array('foo')), array('foo'));
+    $data[] = array(array('redirect' => array('bar', array('query' => array('foo' => 'baz')))), array('bar', array('query' => array('foo' => 'baz'))));
+    $data[] = array(array('redirect' => array('baz', array(), 301)), array('baz', array(), 301));
+
+    $redirect = new RedirectResponse('/example');
+    $data[] = array(array('redirect' => $redirect), $redirect);
+
+    $data[] = array(array('redirect_route' => array('route_name' => 'test_route_a')), new Url('test_route_a', array(), array('absolute' => TRUE)));
+    $data[] = array(array('redirect_route' => array('route_name' => 'test_route_b', 'route_parameters' => array('key' => 'value'))), new Url('test_route_b', array('key' => 'value'), array('absolute' => TRUE)));
+    $data[] = array(array('redirect_route' => new Url('test_route_b', array('key' => 'value'))), new Url('test_route_b', array('key' => 'value'), array('absolute' => TRUE)));
+
+    $data[] = array(array('programmed' => TRUE), NULL);
+    $data[] = array(array('rebuild' => TRUE), NULL);
+    $data[] = array(array('no_redirect' => TRUE), NULL);
+    $data[] = array(array('redirect' => FALSE), NULL);
+
+    return $data;
+  }
+
+  /**
+   * Tests the setError() method.
+   *
+   * @covers ::setError
+   */
+  public function testSetError() {
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
+    $form_state->expects($this->once())
+      ->method('drupalSetMessage')
+      ->willReturn('Fail');
+
+    $element['#parents'] = array('foo', 'bar');
+    $form_state->setError($element, 'Fail');
+  }
+
+  /**
+   * Tests the getError() method.
+   *
+   * @covers ::getError
+   *
+   * @dataProvider providerTestGetError
+   */
+  public function testGetError($errors, $parents, $error = NULL) {
+    $element['#parents'] = $parents;
+    $form_state = new FormState(array(
+      'errors' => $errors,
+    ));
+    $this->assertSame($error, $form_state->getError($element));
+  }
+
+  public function providerTestGetError() {
+    return array(
+      array(array(), array('foo')),
+      array(array('foo][bar' => 'Fail'), array()),
+      array(array('foo][bar' => 'Fail'), array('foo')),
+      array(array('foo][bar' => 'Fail'), array('bar')),
+      array(array('foo][bar' => 'Fail'), array('baz')),
+      array(array('foo][bar' => 'Fail'), array('foo', 'bar'), 'Fail'),
+      array(array('foo][bar' => 'Fail'), array('foo', 'bar', 'baz'), 'Fail'),
+      array(array('foo][bar' => 'Fail 2'), array('foo')),
+      array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo'), 'Fail 1'),
+      array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo', 'bar'), 'Fail 1'),
+    );
+  }
+
+  /**
+   * @covers ::setErrorByName
+   *
+   * @dataProvider providerTestSetErrorByName
+   */
+  public function testSetErrorByName($limit_validation_errors, $expected_errors, $set_message = FALSE) {
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setConstructorArgs(array(array('limit_validation_errors' => $limit_validation_errors)))
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
+    $form_state->clearErrors();
+    $form_state->expects($set_message ? $this->once() : $this->never())
+      ->method('drupalSetMessage');
+
+    $form_state->setErrorByName('test', 'Fail 1');
+    $form_state->setErrorByName('test', 'Fail 2');
+    $form_state->setErrorByName('options');
+
+    $this->assertSame(!empty($expected_errors), $form_state::hasAnyErrors());
+    $this->assertSame($expected_errors, $form_state['errors']);
+  }
+
+  public function providerTestSetErrorByName() {
+    return array(
+      // Only validate the 'options' element.
+      array(array(array('options')), array('options' => '')),
+      // Do not limit an validation, and, ensuring the first error is returned
+      // for the 'test' element.
+      array(NULL, array('test' => 'Fail 1', 'options' => ''), TRUE),
+      // Limit all validation.
+      array(array(), array()),
+    );
+  }
+
+  /**
+   * Tests that form errors during submission throw an exception.
+   *
+   * @covers ::setErrorByName
+   *
+   * @expectedException \LogicException
+   * @expectedExceptionMessage Form errors cannot be set after form validation has finished.
+   */
+  public function testFormErrorsDuringSubmission() {
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setConstructorArgs(array(array('validation_complete' => TRUE)))
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
+    $form_state->setErrorByName('test', 'message');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
index cc34f74..0bdc009 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Form;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Form\FormState;
 use Drupal\Core\Url;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -41,7 +42,7 @@ public function setUp() {
   public function testHandleFormSubmissionNotSubmitted() {
     $form_submitter = $this->getFormSubmitter();
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
 
     $return = $form_submitter->doSubmitForm($form, $form_state);
     $this->assertFalse($form_state['executed']);
@@ -54,9 +55,10 @@ public function testHandleFormSubmissionNotSubmitted() {
   public function testHandleFormSubmissionNoRedirect() {
     $form_submitter = $this->getFormSubmitter();
     $form = array();
-    $form_state = $this->getFormStateDefaults();
-    $form_state['submitted'] = TRUE;
-    $form_state['no_redirect'] = TRUE;
+    $form_state = new FormState(array(
+      'submitted' => TRUE,
+      'no_redirect' => TRUE,
+    ));
 
     $return = $form_submitter->doSubmitForm($form, $form_state);
     $this->assertTrue($form_state['executed']);
@@ -76,9 +78,10 @@ public function testHandleFormSubmissionWithResponses($class, $form_state_key) {
       ->method('prepare')
       ->will($this->returnValue($response));
 
-    $form_state = $this->getFormStateDefaults();
-    $form_state['submitted'] = TRUE;
-    $form_state[$form_state_key] = $response;
+    $form_state = new FormState(array(
+      'submitted' => TRUE,
+      $form_state_key => $response,
+    ));
 
     $form_submitter = $this->getFormSubmitter();
     $form = array();
@@ -101,7 +104,7 @@ public function providerTestHandleFormSubmissionWithResponses() {
    *
    * @dataProvider providerTestRedirectWithResult
    */
-  public function testRedirectWithResult($form_state, $result, $status = 303) {
+  public function testRedirectWithResult($redirect_value, $result, $status = 303) {
     $form_submitter = $this->getFormSubmitter();
     $this->urlGenerator->expects($this->once())
       ->method('generateFromPath')
@@ -113,20 +116,39 @@ public function testRedirectWithResult($form_state, $result, $status = 303) {
         ))
       );
 
-    $form_state += $this->getFormStateDefaults();
+    $form_state = $this->getMock('Drupal\Core\Form\FormStateInterface');
+    $form_state->expects($this->once())
+      ->method('getRedirect')
+      ->willReturn($redirect_value);
     $redirect = $form_submitter->redirectForm($form_state);
     $this->assertSame($result, $redirect->getTargetUrl());
     $this->assertSame($status, $redirect->getStatusCode());
   }
 
   /**
+   * Provides test data for testing the redirectForm() method with a redirect.
+   *
+   * @return array
+   *   Returns some test data.
+   */
+  public function providerTestRedirectWithResult() {
+    return array(
+      array(NULL, '<front>'),
+      array('foo', 'foo'),
+      array(array('foo'), 'foo'),
+      array(array('bar', array('query' => array('foo' => 'baz'))), 'bar'),
+      array(array('baz', array(), 301), 'baz', 301),
+    );
+  }
+
+  /**
    * Tests the redirectForm() with redirect_route when a redirect is expected.
    *
    * @covers ::redirectForm
    *
    * @dataProvider providerTestRedirectWithRouteWithResult
    */
-  public function testRedirectWithRouteWithResult($form_state, $result, $status = 303) {
+  public function testRedirectWithRouteWithResult($redirect_value, $result, $status = 303) {
     $container = new ContainerBuilder();
     $container->set('url_generator', $this->urlGenerator);
     \Drupal::setContainer($container);
@@ -139,64 +161,16 @@ public function testRedirectWithRouteWithResult($form_state, $result, $status =
         ))
       );
 
-    $form_state += $this->getFormStateDefaults();
+    $form_state = $this->getMock('Drupal\Core\Form\FormStateInterface');
+    $form_state->expects($this->once())
+      ->method('getRedirect')
+      ->willReturn($redirect_value);
     $redirect = $form_submitter->redirectForm($form_state);
     $this->assertSame($result, $redirect->getTargetUrl());
     $this->assertSame($status, $redirect->getStatusCode());
   }
 
   /**
-   * Tests the redirectForm() method with a response object.
-   *
-   * @covers ::redirectForm
-   */
-  public function testRedirectWithResponseObject() {
-    $form_submitter = $this->getFormSubmitter();
-    $redirect = new RedirectResponse('/example');
-    $form_state['redirect'] = $redirect;
-
-    $form_state += $this->getFormStateDefaults();
-    $result_redirect = $form_submitter->redirectForm($form_state);
-
-    $this->assertSame($redirect, $result_redirect);
-  }
-
-  /**
-   * Tests the redirectForm() method when no redirect is expected.
-   *
-   * @covers ::redirectForm
-   *
-   * @dataProvider providerTestRedirectWithoutResult
-   */
-  public function testRedirectWithoutResult($form_state) {
-    $form_submitter = $this->getFormSubmitter();
-    $this->urlGenerator->expects($this->never())
-      ->method('generateFromPath');
-    $this->urlGenerator->expects($this->never())
-      ->method('generateFromRoute');
-    $form_state += $this->getFormStateDefaults();
-    $redirect = $form_submitter->redirectForm($form_state);
-    $this->assertNull($redirect);
-  }
-
-  /**
-   * Provides test data for testing the redirectForm() method with a redirect.
-   *
-   * @return array
-   *   Returns some test data.
-   */
-  public function providerTestRedirectWithResult() {
-    return array(
-      array(array(), '<front>'),
-      array(array('redirect' => 'foo'), 'foo'),
-      array(array('redirect' => array('foo')), 'foo'),
-      array(array('redirect' => array('foo')), 'foo'),
-      array(array('redirect' => array('bar', array('query' => array('foo' => 'baz')))), 'bar'),
-      array(array('redirect' => array('baz', array(), 301)), 'baz', 301),
-    );
-  }
-
-  /**
    * Provides test data for testing the redirectForm() method with a route name.
    *
    * @return array
@@ -204,25 +178,46 @@ public function providerTestRedirectWithResult() {
    */
   public function providerTestRedirectWithRouteWithResult() {
     return array(
-      array(array('redirect_route' => array('route_name' => 'test_route_a')), 'test-route'),
-      array(array('redirect_route' => array('route_name' => 'test_route_b', 'route_parameters' => array('key' => 'value'))), 'test-route/value'),
-      array(array('redirect_route' => new Url('test_route_b', array('key' => 'value'))), 'test-route/value'),
+      array(new Url('test_route_a', array(), array('absolute' => TRUE)), 'test-route'),
+      array(new Url('test_route_b', array('key' => 'value'), array('absolute' => TRUE)), 'test-route/value'),
     );
   }
 
   /**
-   * Provides test data for testing the redirectForm() method with no redirect.
+   * Tests the redirectForm() method with a response object.
    *
-   * @return array
-   *   Returns some test data.
+   * @covers ::redirectForm
    */
-  public function providerTestRedirectWithoutResult() {
-    return array(
-      array(array('programmed' => TRUE)),
-      array(array('rebuild' => TRUE)),
-      array(array('no_redirect' => TRUE)),
-      array(array('redirect' => FALSE)),
-    );
+  public function testRedirectWithResponseObject() {
+    $form_submitter = $this->getFormSubmitter();
+    $redirect = new RedirectResponse('/example');
+    $form_state = $this->getMock('Drupal\Core\Form\FormStateInterface');
+    $form_state->expects($this->once())
+      ->method('getRedirect')
+      ->willReturn($redirect);
+
+    $result_redirect = $form_submitter->redirectForm($form_state);
+
+    $this->assertSame($redirect, $result_redirect);
+  }
+
+  /**
+   * Tests the redirectForm() method when no redirect is expected.
+   *
+   * @covers ::redirectForm
+   */
+  public function testRedirectWithoutResult() {
+    $form_submitter = $this->getFormSubmitter();
+    $this->urlGenerator->expects($this->never())
+      ->method('generateFromPath');
+    $this->urlGenerator->expects($this->never())
+      ->method('generateFromRoute');
+    $form_state = $this->getMock('Drupal\Core\Form\FormStateInterface');
+    $form_state->expects($this->once())
+      ->method('getRedirect')
+      ->willReturn(FALSE);
+    $redirect = $form_submitter->redirectForm($form_state);
+    $this->assertNull($redirect);
   }
 
   /**
@@ -233,13 +228,13 @@ public function testExecuteSubmitHandlers() {
     $mock = $this->getMock('stdClass', array('submit_handler', 'hash_submit'));
     $mock->expects($this->once())
       ->method('submit_handler')
-      ->with($this->isType('array'), $this->isType('array'));
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
     $mock->expects($this->once())
       ->method('hash_submit')
-      ->with($this->isType('array'), $this->isType('array'));
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_submitter->executeSubmitHandlers($form, $form_state);
 
     $form['#submit'][] = array($mock, 'hash_submit');
@@ -251,17 +246,6 @@ public function testExecuteSubmitHandlers() {
   }
 
   /**
-   * @return array()
-   */
-  protected function getFormStateDefaults() {
-    $form_builder = $this->getMockBuilder('Drupal\Core\Form\FormBuilder')
-      ->disableOriginalConstructor()
-      ->setMethods(NULL)
-      ->getMock();
-    return $form_builder->getFormStateDefaults();
-  }
-
-  /**
    * @return \Drupal\Core\Form\FormSubmitterInterface
    */
   protected function getFormSubmitter() {
diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
index 0870788..a0e9f47 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Form {
 
 use Drupal\Component\Utility\String;
+use Drupal\Core\Form\FormState;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -19,23 +20,6 @@
 class FormValidatorTest extends UnitTestCase {
 
   /**
-   * Tests that form errors during submission throw an exception.
-   *
-   * @covers ::setErrorByName
-   *
-   * @expectedException \LogicException
-   * @expectedExceptionMessage Form errors cannot be set after form validation has finished.
-   */
-  public function testFormErrorsDuringSubmission() {
-    $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
-      ->disableOriginalConstructor()
-      ->setMethods(NULL)
-      ->getMock();
-    $form_state['validation_complete'] = TRUE;
-    $form_validator->setErrorByName('test', $form_state, 'message');
-  }
-
-  /**
    * Tests the 'validation_complete' $form_state flag.
    *
    * @covers ::validateForm
@@ -48,7 +32,7 @@ public function testValidationComplete() {
       ->getMock();
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $this->assertFalse($form_state['validation_complete']);
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertTrue($form_state['validation_complete']);
@@ -68,7 +52,7 @@ public function testPreventDuplicateValidation() {
       ->method('doValidateForm');
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['validation_complete'] = TRUE;
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertArrayNotHasKey('#errors', $form);
@@ -88,7 +72,7 @@ public function testMustValidate() {
       ->method('doValidateForm');
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['validation_complete'] = TRUE;
     $form_state['must_validate'] = TRUE;
     $form_validator->validateForm('test_form_id', $form, $form_state);
@@ -115,12 +99,12 @@ public function testValidateInvalidFormToken() {
       ->getMock();
     $form_validator->expects($this->once())
       ->method('setErrorByName')
-      ->with('form_token', $this->isType('array'), 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');
+      ->with('form_token', $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'), 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');
     $form_validator->expects($this->never())
       ->method('doValidateForm');
 
     $form['#token'] = 'test_form_id';
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['values']['form_token'] = 'some_random_token';
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertTrue($form_state['validation_complete']);
@@ -148,120 +132,19 @@ public function testValidateValidFormToken() {
       ->method('doValidateForm');
 
     $form['#token'] = 'test_form_id';
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['values']['form_token'] = 'some_random_token';
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertTrue($form_state['validation_complete']);
   }
 
   /**
-   * Tests the setError() method.
-   *
-   * @covers ::setError
-   */
-  public function testSetError() {
-    $form_state = $this->getFormStateDefaults();
-
-    $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
-      ->disableOriginalConstructor()
-      ->setMethods(array('setErrorByName'))
-      ->getMock();
-    $form_validator->expects($this->once())
-      ->method('setErrorByName')
-      ->with('foo][bar', $form_state, 'Fail');
-
-    $element['#parents'] = array('foo', 'bar');
-    $form_validator->setError($element, $form_state, 'Fail');
-  }
-
-  /**
-   * Tests the getError() method.
-   *
-   * @covers ::getError
-   *
-   * @dataProvider providerTestGetError
-   */
-  public function testGetError($errors, $parents, $error = NULL) {
-    $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
-      ->disableOriginalConstructor()
-      ->setMethods(NULL)
-      ->getMock();
-
-    $element['#parents'] = $parents;
-    $form_state = $this->getFormStateDefaults();
-    $form_state['errors'] = $errors;
-    $this->assertSame($error, $form_validator->getError($element, $form_state));
-  }
-
-  public function providerTestGetError() {
-    return array(
-      array(array(), array('foo')),
-      array(array('foo][bar' => 'Fail'), array()),
-      array(array('foo][bar' => 'Fail'), array('foo')),
-      array(array('foo][bar' => 'Fail'), array('bar')),
-      array(array('foo][bar' => 'Fail'), array('baz')),
-      array(array('foo][bar' => 'Fail'), array('foo', 'bar'), 'Fail'),
-      array(array('foo][bar' => 'Fail'), array('foo', 'bar', 'baz'), 'Fail'),
-      array(array('foo][bar' => 'Fail 2'), array('foo')),
-      array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo'), 'Fail 1'),
-      array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo', 'bar'), 'Fail 1'),
-    );
-  }
-
-  /**
-   * @covers ::setErrorByName
-   *
-   * @dataProvider providerTestSetErrorByName
-   */
-  public function testSetErrorByName($limit_validation_errors, $expected_errors, $set_message = FALSE) {
-    $request_stack = new RequestStack();
-    $request = new Request();
-    $request_stack->push($request);
-    $csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
-      ->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
-      ->setMethods(array('drupalSetMessage'))
-      ->getMock();
-    $form_validator->expects($set_message ? $this->once() : $this->never())
-      ->method('drupalSetMessage');
-
-    $form_state = $this->getFormStateDefaults();
-    $form_state['limit_validation_errors'] = $limit_validation_errors;
-    $form_validator->setErrorByName('test', $form_state, 'Fail 1');
-    $form_validator->setErrorByName('test', $form_state, 'Fail 2');
-    $form_validator->setErrorByName('options', $form_state);
-
-    $this->assertSame(!empty($expected_errors), $request->attributes->get('_form_errors', FALSE));
-    $this->assertSame($expected_errors, $form_state['errors']);
-  }
-
-  public function providerTestSetErrorByName() {
-    return array(
-      // Only validate the 'options' element.
-      array(array(array('options')), array('options' => '')),
-      // Do not limit an validation, and, ensuring the first error is returned
-      // for the 'test' element.
-      array(NULL, array('test' => 'Fail 1', 'options' => ''), TRUE),
-      // Limit all validation.
-      array(array(), array()),
-    );
-  }
-
-  /**
    * @covers ::setElementErrorsFromFormState
    */
   public function testSetElementErrorsFromFormState() {
-    $request_stack = new RequestStack();
-    $request = new Request();
-    $request_stack->push($request);
-    $csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
-      ->disableOriginalConstructor()
-      ->getMock();
     $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
-      ->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
-      ->setMethods(array('drupalSetMessage'))
+      ->disableOriginalConstructor()
+      ->setMethods(NULL)
       ->getMock();
 
     $form = array(
@@ -272,7 +155,9 @@ public function testSetElementErrorsFromFormState() {
       '#title' => 'Test',
       '#parents' => array('test'),
     );
-    $form_state = $this->getFormStateDefaults();
+    $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
     $form_validator->setErrorByName('test', $form_state, 'invalid');
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertSame('invalid', $form['test']['#errors']);
@@ -290,7 +175,7 @@ public function testHandleErrorsWithLimitedValidation($sections, $triggering_ele
       ->getMock();
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['triggering_element'] = $triggering_element;
     $form_state['triggering_element']['#limit_validation_errors'] = $sections;
 
@@ -387,13 +272,13 @@ public function testExecuteValidateHandlers() {
     $mock = $this->getMock('stdClass', array('validate_handler', 'hash_validate'));
     $mock->expects($this->once())
       ->method('validate_handler')
-      ->with($this->isType('array'), $this->isType('array'));
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
     $mock->expects($this->once())
       ->method('hash_validate')
-      ->with($this->isType('array'), $this->isType('array'));
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
 
     $form = array();
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_validator->executeValidateHandlers($form, $form_state);
 
     $form['#validate'][] = array($mock, 'hash_validate');
@@ -415,13 +300,13 @@ public function testRequiredErrorMessage($element, $expected_message) {
       ->getMock();
     $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
       ->setConstructorArgs(array(new RequestStack(), $this->getStringTranslationStub(), $csrf_token))
-      ->setMethods(array('executeValidateHandlers', 'setErrorByName'))
+      ->setMethods(array('executeValidateHandlers', 'setError'))
       ->getMock();
     $form_validator->expects($this->once())
       ->method('executeValidateHandlers');
     $form_validator->expects($this->once())
-      ->method('setErrorByName')
-      ->with('test', $this->isType('array'), $expected_message);
+      ->method('setError')
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'), $expected_message);
 
     $form = array();
     $form['test'] = $element + array(
@@ -431,7 +316,7 @@ public function testRequiredErrorMessage($element, $expected_message) {
       '#required' => TRUE,
       '#parents' => array('test'),
     );
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_validator->validateForm('test_form_id', $form, $form_state);
   }
 
@@ -468,7 +353,7 @@ public function testElementValidate() {
     $mock = $this->getMock('stdClass', array('element_validate'));
     $mock->expects($this->once())
       ->method('element_validate')
-      ->with($this->isType('array'), $this->isType('array'), NULL);
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'), NULL);
 
     $form = array();
     $form['test'] = array(
@@ -477,7 +362,7 @@ public function testElementValidate() {
       '#parents' => array('test'),
       '#element_validate' => array(array($mock, 'element_validate')),
     );
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_validator->validateForm('test_form_id', $form, $form_state);
   }
 
@@ -492,11 +377,11 @@ public function testPerformRequiredValidation($element, $expected_message, $call
       ->getMock();
     $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
       ->setConstructorArgs(array(new RequestStack(), $this->getStringTranslationStub(), $csrf_token))
-      ->setMethods(array('setErrorByName', 'watchdog'))
+      ->setMethods(array('setError', 'watchdog'))
       ->getMock();
     $form_validator->expects($this->once())
-      ->method('setErrorByName')
-      ->with('test', $this->isType('array'), $expected_message);
+      ->method('setError')
+      ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'), $expected_message);
 
     if ($call_watchdog) {
       $form_validator->expects($this->once())
@@ -511,7 +396,7 @@ public function testPerformRequiredValidation($element, $expected_message, $call
       '#required' => FALSE,
       '#parents' => array('test'),
     );
-    $form_state = $this->getFormStateDefaults();
+    $form_state = new FormState();
     $form_state['values'] = array();
     $form_validator->validateForm('test_form_id', $form, $form_state);
   }
@@ -584,17 +469,6 @@ public function providerTestPerformRequiredValidation() {
     );
   }
 
-  /**
-   * @return array()
-   */
-  protected function getFormStateDefaults() {
-    $form_builder = $this->getMockBuilder('Drupal\Core\Form\FormBuilder')
-      ->disableOriginalConstructor()
-      ->setMethods(NULL)
-      ->getMock();
-    return $form_builder->getFormStateDefaults();
-  }
-
 }
 
 }
