Bug 1501152 - Refactor Key::EncodeJSValInternal to show direct correspondence to spec. r=asuth,tcampbell, a=jcristau FIREFOX_60_9_0esr_BUILD1 FIREFOX_60_9_0esr_RELEASE
authorYaron Tausky <ytausky@mozilla.com>
Thu, 22 Aug 2019 09:31:51 +0200
changeset 451313 d8dd10ac54302e4bd9cf6c537f790933b149099d
parent 451312 2ce17180fd4dd8828caa964ba55205a51c313355
child 451314 827b2e4141cb091b6b057e2db095c72db4868625
child 451318 887a438d43fa73e603704d02ea6756ea4e69eb1d
push id493
push userjcristau@mozilla.com
push dateSun, 01 Sep 2019 09:46:03 +0000
treeherdermozilla-esr60@d8dd10ac5430 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth, tcampbell, jcristau
bugs1501152
milestone60.9.0
Bug 1501152 - Refactor Key::EncodeJSValInternal to show direct correspondence to spec. r=asuth,tcampbell, a=jcristau This commit adds the text of the spec as inline comments and refactors the code such that it directly corresponds to the spec's steps. This makes it easier to understand how the spec's algorithm is implemented. Differential Revision: https://phabricator.services.mozilla.com/D43016
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/IDBCursor.cpp
dom/indexedDB/IDBFactory.cpp
dom/indexedDB/IDBKeyRange.cpp
dom/indexedDB/IDBObjectStore.cpp
dom/indexedDB/IDBResult.h
dom/indexedDB/Key.cpp
dom/indexedDB/Key.h
dom/indexedDB/KeyPath.cpp
dom/indexedDB/moz.build
dom/indexedDB/test/gtest/TestIDBResult.cpp
dom/indexedDB/test/gtest/moz.build
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -2102,17 +2102,22 @@ class EncodeKeysFunction final : public 
     Key key;
     if (type == mozIStorageStatement::VALUE_TYPE_INTEGER) {
       int64_t intKey;
       aArguments->GetInt64(0, &intKey);
       key.SetFromInteger(intKey);
     } else if (type == mozIStorageStatement::VALUE_TYPE_TEXT) {
       nsString stringKey;
       aArguments->GetString(0, stringKey);
-      key.SetFromString(stringKey);
+      ErrorResult errorResult;
+      auto result = key.SetFromString(stringKey, errorResult);
+      if (!result.Is(Ok, errorResult)) {
+        return result.Is(Invalid, errorResult) ? NS_ERROR_DOM_INDEXEDDB_DATA_ERR
+                                               : errorResult.StealNSResult();
+      }
     } else {
       NS_WARNING("Don't call me with the wrong type of arguments!");
       return NS_ERROR_UNEXPECTED;
     }
 
     const nsCString& buffer = key.GetBuffer();
 
     std::pair<const void*, int> data(static_cast<const void*>(buffer.get()),
@@ -17709,36 +17714,44 @@ nsresult DatabaseOperationBase::BindKeyR
   MOZ_ASSERT(!IsOnBackgroundThread());
   MOZ_ASSERT(aStatement);
   MOZ_ASSERT(!aLocale.IsEmpty());
 
   nsresult rv = NS_OK;
 
   if (!aKeyRange.lower().IsUnset()) {
     Key lower;
-    rv = aKeyRange.lower().ToLocaleBasedKey(lower, aLocale);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    ErrorResult errorResult;
+    auto result =
+        aKeyRange.lower().ToLocaleBasedKey(lower, aLocale, errorResult);
+    if (!result.Is(Ok, errorResult)) {
+      return NS_WARN_IF(result.Is(Exception, errorResult))
+                 ? errorResult.StealNSResult()
+                 : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
     }
 
     rv = lower.BindToStatement(aStatement, NS_LITERAL_CSTRING("lower_key"));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
   if (aKeyRange.isOnly()) {
     return rv;
   }
 
   if (!aKeyRange.upper().IsUnset()) {
     Key upper;
-    rv = aKeyRange.upper().ToLocaleBasedKey(upper, aLocale);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    ErrorResult errorResult;
+    auto result =
+        aKeyRange.upper().ToLocaleBasedKey(upper, aLocale, errorResult);
+    if (!result.Is(Ok, errorResult)) {
+      return NS_WARN_IF(result.Is(Exception, errorResult))
+                 ? errorResult.StealNSResult()
+                 : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
     }
 
     rv = upper.BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key"));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
@@ -19800,19 +19813,22 @@ nsresult OpenDatabaseOp::UpdateLocaleAwa
       return rv;
     }
 
     rv = oldKey.BindToStatement(writeStmt, NS_LITERAL_CSTRING("value"));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
-    rv = oldKey.ToLocaleBasedKey(newSortKey, aLocale);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    ErrorResult errorResult;
+    auto result = oldKey.ToLocaleBasedKey(newSortKey, aLocale, errorResult);
+    if (!result.Is(Ok, errorResult)) {
+      return NS_WARN_IF(result.Is(Exception, errorResult))
+                 ? errorResult.StealNSResult()
+                 : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
     }
 
     rv = newSortKey.BindToStatement(writeStmt,
                                     NS_LITERAL_CSTRING("value_locale"));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
@@ -25133,34 +25149,41 @@ nsresult Cursor::CursorOpBase::PopulateR
 
 void Cursor::OpenOp::GetRangeKeyInfo(bool aLowerBound, Key* aKey, bool* aOpen) {
   AssertIsOnConnectionThread();
   MOZ_ASSERT(aKey);
   MOZ_ASSERT(aKey->IsUnset());
   MOZ_ASSERT(aOpen);
 
   if (mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) {
+    ErrorResult rv;
     const SerializedKeyRange& range =
         mOptionalKeyRange.get_SerializedKeyRange();
     if (range.isOnly()) {
       *aKey = range.lower();
       *aOpen = false;
       if (mCursor->IsLocaleAware()) {
-        range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale);
+        Unused << range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale, rv);
       }
     } else {
       *aKey = aLowerBound ? range.lower() : range.upper();
       *aOpen = aLowerBound ? range.lowerOpen() : range.upperOpen();
       if (mCursor->IsLocaleAware()) {
         if (aLowerBound) {
-          range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale);
+          Unused << range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale, rv);
         } else {
-          range.upper().ToLocaleBasedKey(*aKey, mCursor->mLocale);
-        }
-      }
+          Unused << range.upper().ToLocaleBasedKey(*aKey, mCursor->mLocale, rv);
+        }
+      }
+    }
+
+    // XXX Explain why the error is ignored here (If it's impossible, then we
+    //     should change this to an assertion.)
+    if (rv.Failed()) {
+      rv.SuppressException();
     }
   } else {
     *aOpen = false;
   }
 }
 
 nsresult Cursor::OpenOp::DoObjectStoreDatabaseWork(
     DatabaseConnection* aConnection) {
--- a/dom/indexedDB/IDBCursor.cpp
+++ b/dom/indexedDB/IDBCursor.cpp
@@ -384,25 +384,31 @@ void IDBCursor::Continue(JSContext* aCx,
   }
 
   if (IsSourceDeleted() || !mHaveValue || mContinueCalled) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
     return;
   }
 
   Key key;
-  key.SetFromJSVal(aCx, aKey, aRv);
-  if (aRv.Failed()) {
+  auto result = key.SetFromJSVal(aCx, aKey, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return;
   }
 
   if (IsLocaleAware() && !key.IsUnset()) {
     Key tmp;
-    aRv = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale());
-    if (aRv.Failed()) {
+    result = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale(), aRv);
+    if (!result.Is(Ok, aRv)) {
+      if (result.Is(Invalid, aRv)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+      }
       return;
     }
     key = tmp;
   }
 
   const Key& sortKey = IsLocaleAware() ? mSortKey : mKey;
 
   if (!key.IsUnset()) {
@@ -482,40 +488,49 @@ void IDBCursor::ContinuePrimaryKey(JSCon
   }
 
   if (!mHaveValue || mContinueCalled) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
     return;
   }
 
   Key key;
-  key.SetFromJSVal(aCx, aKey, aRv);
-  if (aRv.Failed()) {
+  auto result = key.SetFromJSVal(aCx, aKey, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return;
   }
 
   if (IsLocaleAware() && !key.IsUnset()) {
     Key tmp;
-    aRv = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale());
-    if (aRv.Failed()) {
+    result = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale(), aRv);
+    if (!result.Is(Ok, aRv)) {
+      if (result.Is(Invalid, aRv)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+      }
       return;
     }
     key = tmp;
   }
 
   const Key& sortKey = IsLocaleAware() ? mSortKey : mKey;
 
   if (key.IsUnset()) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
     return;
   }
 
   Key primaryKey;
-  primaryKey.SetFromJSVal(aCx, aPrimaryKey, aRv);
-  if (aRv.Failed()) {
+  result = primaryKey.SetFromJSVal(aCx, aPrimaryKey, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return;
   }
 
   if (primaryKey.IsUnset()) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
     return;
   }
 
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -468,23 +468,29 @@ already_AddRefed<IDBOpenDBRequest> IDBFa
                       /* aPrincipal */ nullptr, aName, Optional<uint64_t>(),
                       aOptions.mStorage,
                       /* aDeleting */ true, aCallerType, aRv);
 }
 
 int16_t IDBFactory::Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst,
                         JS::Handle<JS::Value> aSecond, ErrorResult& aRv) {
   Key first, second;
-  first.SetFromJSVal(aCx, aFirst, aRv);
-  if (aRv.Failed()) {
+  auto result = first.SetFromJSVal(aCx, aFirst, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return 0;
   }
 
-  second.SetFromJSVal(aCx, aSecond, aRv);
-  if (aRv.Failed()) {
+  result = second.SetFromJSVal(aCx, aSecond, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return 0;
   }
 
   if (first.IsUnset() || second.IsUnset()) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
     return 0;
   }
 
--- a/dom/indexedDB/IDBKeyRange.cpp
+++ b/dom/indexedDB/IDBKeyRange.cpp
@@ -16,18 +16,21 @@ namespace mozilla {
 namespace dom {
 
 using namespace mozilla::dom::indexedDB;
 
 namespace {
 
 void GetKeyFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal, Key& aKey,
                      ErrorResult& aRv) {
-  aKey.SetFromJSVal(aCx, aVal, aRv);
-  if (aRv.Failed()) {
+  auto result = aKey.SetFromJSVal(aCx, aVal, aRv);
+  if (!result.Is(Ok, aRv)) {
+    if (result.Is(Invalid, aRv)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    }
     return;
   }
 
   if (aKey.IsUnset()) {
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
   }
 }
 
--- a/dom/indexedDB/IDBObjectStore.cpp
+++ b/dom/indexedDB/IDBObjectStore.cpp
@@ -1263,19 +1263,22 @@ void IDBObjectStore::AppendIndexUpdateIn
     if (aRv.Failed()) {
       return;
     }
 
     IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
     updateInfo->indexId() = aIndexID;
     updateInfo->value() = key;
     if (localeAware) {
-      aRv = key.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale);
-      if (NS_WARN_IF(aRv.Failed())) {
-        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+      auto result =
+          key.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale, aRv);
+      if (NS_WARN_IF(!result.Is(Ok, aRv))) {
+        if (result.Is(Invalid, aRv)) {
+          aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        }
         return;
       }
     }
 
     return;
   }
 
   JS::Rooted<JS::Value> val(aCx);
@@ -1302,50 +1305,56 @@ void IDBObjectStore::AppendIndexUpdateIn
       JS::Rooted<JS::Value> arrayItem(aCx);
       if (NS_WARN_IF(!JS_GetElement(aCx, array, arrayIndex, &arrayItem))) {
         IDB_REPORT_INTERNAL_ERR();
         aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
         return;
       }
 
       Key value;
-      value.SetFromJSVal(aCx, arrayItem, aRv);
-      if (aRv.Failed() || value.IsUnset()) {
+      auto result = value.SetFromJSVal(aCx, arrayItem, aRv);
+      if (!result.Is(Ok, aRv) || value.IsUnset()) {
         // Not a value we can do anything with, ignore it.
         aRv.SuppressException();
         continue;
       }
 
       IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
       updateInfo->indexId() = aIndexID;
       updateInfo->value() = value;
       if (localeAware) {
-        aRv = value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale);
-        if (NS_WARN_IF(aRv.Failed())) {
-          aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        auto result =
+            value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale, aRv);
+        if (NS_WARN_IF(!result.Is(Ok, aRv))) {
+          if (result.Is(Invalid, aRv)) {
+            aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+          }
           return;
         }
       }
     }
   } else {
     Key value;
-    value.SetFromJSVal(aCx, val, aRv);
-    if (aRv.Failed() || value.IsUnset()) {
+    auto result = value.SetFromJSVal(aCx, val, aRv);
+    if (!result.Is(Ok, aRv) || value.IsUnset()) {
       // Not a value we can do anything with, ignore it.
       aRv.SuppressException();
       return;
     }
 
     IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
     updateInfo->indexId() = aIndexID;
     updateInfo->value() = value;
     if (localeAware) {
-      aRv = value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale);
-      if (NS_WARN_IF(aRv.Failed())) {
-        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+      auto result =
+          value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale, aRv);
+      if (NS_WARN_IF(!result.Is(Ok, aRv))) {
+        if (result.Is(Invalid, aRv)) {
+          aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        }
         return;
       }
     }
   }
 }
 
 // static
 void IDBObjectStore::ClearCloneReadInfo(StructuredCloneReadInfo& aReadInfo) {
@@ -1731,18 +1740,21 @@ void IDBObjectStore::GetAddInfo(JSContex
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
     return;
   }
 
   bool isAutoIncrement = AutoIncrement();
 
   if (!HasValidKeyPath()) {
     // Out-of-line keys must be passed in.
-    aKey.SetFromJSVal(aCx, aKeyVal, aRv);
-    if (aRv.Failed()) {
+    auto result = aKey.SetFromJSVal(aCx, aKeyVal, aRv);
+    if (!result.Is(Ok, aRv)) {
+      if (result.Is(Invalid, aRv)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+      }
       return;
     }
   } else if (!isAutoIncrement) {
     if (!aValueWrapper.Clone(aCx)) {
       aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR);
       return;
     }
 
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/IDBResult.h
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_indexeddb_idbresult_h__
+#define mozilla_dom_indexeddb_idbresult_h__
+
+#include <mozilla/ErrorResult.h>
+#include <mozilla/Variant.h>
+
+#include <type_traits>
+#include <utility>
+
+namespace mozilla {
+namespace dom {
+namespace indexedDB {
+
+// IDBSpecialValue represents two special return values, distinct from any other
+// value, used in several places in the IndexedDB spec.
+enum class IDBSpecialValue {
+  Failure,
+  Invalid,
+};
+
+namespace detail {
+template <typename T>
+struct OkType final {
+  T mValue;
+};
+
+template <>
+struct OkType<void> final {};
+
+template <IDBSpecialValue Value>
+using SpecialConstant = std::integral_constant<IDBSpecialValue, Value>;
+using FailureType = detail::SpecialConstant<IDBSpecialValue::Failure>;
+using InvalidType = detail::SpecialConstant<IDBSpecialValue::Invalid>;
+struct ExceptionType final {};
+struct VoidType final {};
+}  // namespace detail
+
+namespace {
+template <typename T>
+constexpr inline detail::OkType<std::remove_reference_t<T>> Ok(T&& aValue) {
+  return {std::forward<T>(aValue)};
+}
+
+constexpr inline detail::OkType<void> Ok() { return {}; }
+
+constexpr const detail::FailureType Failure;
+constexpr const detail::InvalidType Invalid;
+constexpr const detail::ExceptionType Exception;
+}  // namespace
+
+namespace detail {
+template <IDBSpecialValue... Elements>
+struct IsSortedSet;
+
+template <IDBSpecialValue First, IDBSpecialValue Second,
+          IDBSpecialValue... Rest>
+struct IsSortedSet<First, Second, Rest...>
+    : std::integral_constant<bool, IsSortedSet<First, Second>::value &&
+                                       IsSortedSet<Second, Rest...>::value> {};
+
+template <IDBSpecialValue First, IDBSpecialValue Second>
+struct IsSortedSet<First, Second>
+    : std::integral_constant<bool, (First < Second)> {};
+
+template <IDBSpecialValue First>
+struct IsSortedSet<First> : std::true_type {};
+
+template <>
+struct IsSortedSet<> : std::true_type {};
+
+// IDBResultBase contains the bulk of the implementation of IDBResult, namely
+// functionality that's applicable to all values of T.
+template <typename T, IDBSpecialValue... S>
+class IDBResultBase {
+  // This assertion ensures that permutations of the set of possible special
+  // values don't create distinct types.
+  static_assert(detail::IsSortedSet<S...>::value,
+                "special value list must be sorted and unique");
+
+  template <typename R, IDBSpecialValue... U>
+  friend class IDBResultBase;
+
+ protected:
+  using ValueType = detail::OkType<T>;
+
+ public:
+  // Construct a normal result. Use the Ok function to create an object of type
+  // ValueType.
+  MOZ_IMPLICIT IDBResultBase(const ValueType& aValue) : mVariant(aValue) {}
+
+  MOZ_IMPLICIT IDBResultBase(detail::ExceptionType)
+      : mVariant(detail::ExceptionType{}) {}
+
+  template <IDBSpecialValue Special>
+  MOZ_IMPLICIT IDBResultBase(detail::SpecialConstant<Special>)
+      : mVariant(detail::SpecialConstant<Special>{}) {}
+
+  using VariantType =
+      Variant<ValueType, detail::ExceptionType, detail::SpecialConstant<S>...>;
+
+  struct Matcher {
+    template <typename V>
+    VariantType match(const V& aVariant) const {
+      return VariantType{aVariant};
+    };
+  };
+
+  // Construct an IDBResult from another IDBResult whose set of possible special
+  // values is a subset of this one's.
+  template <IDBSpecialValue... U>
+  MOZ_IMPLICIT IDBResultBase(const IDBResultBase<T, U...>& aOther)
+     : mVariant(aOther.mVariant.match(Matcher())) {}
+
+  // Test whether the result is a normal return value. The choice of the first
+  // parameter's type makes it possible to write `result.Is(Ok, rv)`, promoting
+  // readability and uniformity with other functions in the overload set.
+  bool Is(detail::OkType<void> (*)(), const ErrorResult& aRv) const {
+    AssertConsistency(aRv);
+    return mVariant.template is<ValueType>();
+  }
+
+  bool Is(detail::ExceptionType, const ErrorResult& aRv) const {
+    AssertConsistency(aRv);
+    return mVariant.template is<detail::ExceptionType>();
+  }
+
+  template <IDBSpecialValue Special>
+  bool Is(detail::SpecialConstant<Special>, const ErrorResult& aRv) const {
+    AssertConsistency(aRv);
+    return mVariant.template is<detail::SpecialConstant<Special>>();
+  }
+
+ protected:
+  void AssertConsistency(const ErrorResult& aRv) const {
+    MOZ_ASSERT(aRv.Failed() == mVariant.template is<detail::ExceptionType>());
+  }
+
+  VariantType mVariant;
+};
+}  // namespace detail
+
+// Represents a return value of an IndexedDB algorithm. T is the type of the
+// regular return value, while S is a list of special values that can be
+// returned by the particular algorithm.
+template <typename T, IDBSpecialValue... S>
+class MOZ_MUST_USE_TYPE IDBResult : public detail::IDBResultBase<T, S...> {
+ public:
+  using IDBResult::IDBResultBase::IDBResultBase;
+
+  // Get a reference to the regular return value, asserting that this object
+  // is indeed a regular return value.
+  T& Unwrap(const ErrorResult& aRv) {
+    return const_cast<T&>(static_cast<const IDBResult*>(this)->Unwrap(aRv));
+  }
+
+  const T& Unwrap(const ErrorResult& aRv) const {
+    this->AssertConsistency(aRv);
+    return this->mVariant.template as<typename IDBResult::ValueType>().mValue;
+  }
+};
+
+template <IDBSpecialValue... S>
+class MOZ_MUST_USE_TYPE IDBResult<void, S...>
+    : public detail::IDBResultBase<void, S...> {
+ public:
+  using IDBResult::IDBResultBase::IDBResultBase;
+};
+
+}  // namespace indexedDB
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_indexeddb_idbresult_h__
--- a/dom/indexedDB/Key.cpp
+++ b/dom/indexedDB/Key.cpp
@@ -103,25 +103,37 @@ namespace indexedDB {
 
  "foo"         // 0x30 s s s
  1             // 0x10 bf f0
  ["a", "b"]    // 0x80 s 0 0x30 s
  [1, 2]        // 0x60 bf f0 0 0 0 0 0 0 0x10 c0
  [[]]          // 0x80
 */
 
-nsresult Key::ToLocaleBasedKey(Key& aTarget, const nsCString& aLocale) const {
+IDBResult<void, IDBSpecialValue::Invalid> Key::SetFromString(
+    const nsAString& aString, ErrorResult& aRv) {
+  mBuffer.Truncate();
+  auto result = EncodeString(aString, 0, aRv);
+  if (result.Is(Ok, aRv)) {
+    TrimBuffer();
+  }
+  return result;
+}
+
+
+IDBResult<void, IDBSpecialValue::Invalid> Key::ToLocaleBasedKey(
+    Key& aTarget, const nsCString& aLocale, ErrorResult& aRv) const {
   if (IsUnset()) {
     aTarget.Unset();
-    return NS_OK;
+    return Ok();
   }
 
   if (IsFloat() || IsDate() || IsBinary()) {
     aTarget.mBuffer = mBuffer;
-    return NS_OK;
+    return Ok();
   }
 
   aTarget.mBuffer.Truncate();
   aTarget.mBuffer.SetCapacity(mBuffer.Length());
 
   auto* it = reinterpret_cast<const unsigned char*>(mBuffer.BeginReading());
   auto* end = reinterpret_cast<const unsigned char*>(mBuffer.EndReading());
 
@@ -140,175 +152,207 @@ nsresult Key::ToLocaleBasedKey(Key& aTar
       canShareBuffers = false;
       break;
     }
   }
 
   if (canShareBuffers) {
     MOZ_ASSERT(it == end);
     aTarget.mBuffer = mBuffer;
-    return NS_OK;
+    return Ok();
   }
 
   // A string was found, so we need to copy the data we've read so far
   auto* start = reinterpret_cast<const unsigned char*>(mBuffer.BeginReading());
   if (it > start) {
     char* buffer;
     if (!aTarget.mBuffer.GetMutableData(&buffer, it - start)) {
-      return NS_ERROR_OUT_OF_MEMORY;
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return Exception;
     }
 
     while (start < it) {
       *(buffer++) = *(start++);
     }
   }
 
   // Now continue decoding
   while (it < end) {
     char* buffer;
     uint32_t oldLen = aTarget.mBuffer.Length();
     auto type = *it % eMaxType;
 
     if (type == eTerminator || type == eArray) {
       // Copy array TypeID and terminator from raw key
       if (!aTarget.mBuffer.GetMutableData(&buffer, oldLen + 1)) {
-        return NS_ERROR_OUT_OF_MEMORY;
+        aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+        return Exception;
       }
       *(buffer + oldLen) = *(it++);
     } else if (type == eFloat || type == eDate) {
       // Copy number from raw key
       if (!aTarget.mBuffer.GetMutableData(&buffer,
                                           oldLen + 1 + sizeof(uint64_t))) {
-        return NS_ERROR_OUT_OF_MEMORY;
+        aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+        return Exception;
       }
       buffer += oldLen;
       *(buffer++) = *(it++);
 
       const size_t byteCount = std::min(sizeof(uint64_t), size_t(end - it));
       for (size_t count = 0; count < byteCount; count++) {
         *(buffer++) = (*it++);
       }
     } else {
       // Decode string and reencode
       uint8_t typeOffset = *it - eString;
       MOZ_ASSERT((typeOffset % eArray == 0) && (typeOffset / eArray <= 2));
 
       nsDependentString str;
       DecodeString(it, end, str);
-      nsresult rv = aTarget.EncodeLocaleString(str, typeOffset, aLocale);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
+      auto result = aTarget.EncodeLocaleString(str, typeOffset, aLocale, aRv);
+      if (NS_WARN_IF(!result.Is(Ok, aRv))) {
+        return result;
       }
     }
   }
   aTarget.TrimBuffer();
-  return NS_OK;
+  return Ok();
 }
 
-nsresult Key::EncodeJSValInternal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                                  uint8_t aTypeOffset,
-                                  uint16_t aRecursionDepth) {
-  static_assert(eMaxType * kMaxArrayCollapse < 256, "Unable to encode jsvals.");
+class MOZ_STACK_CLASS Key::ArrayValueEncoder final {
+ public:
+  ArrayValueEncoder(Key& aKey, const uint8_t aTypeOffset,
+                    const uint16_t aRecursionDepth)
+      : mKey(aKey),
+        mTypeOffset(aTypeOffset),
+        mRecursionDepth(aRecursionDepth) {}
+
+  void AddToSeenSet(JSContext* const aCx, JS::HandleObject) {
+    ++mRecursionDepth;
+  }
 
-  if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) {
-    return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
+  void BeginSubkeyList() {
+    mTypeOffset += Key::eMaxType;
+    if (mTypeOffset == eMaxType * kMaxArrayCollapse) {
+      mKey.mBuffer.Append(mTypeOffset);
+      mTypeOffset = 0;
+    }
+    MOZ_ASSERT(mTypeOffset % eMaxType == 0,
+               "Current type offset must indicate beginning of array");
+    MOZ_ASSERT(mTypeOffset < eMaxType * kMaxArrayCollapse);
+  }
+
+  IDBResult<void, IDBSpecialValue::Invalid> ConvertSubkey(
+      JSContext* const aCx, JS::HandleValue aEntry, const uint32_t aIndex,
+      ErrorResult& aRv) {
+    const auto result = mKey.EncodeJSValInternal(aCx, aEntry, mTypeOffset,
+                                                 mRecursionDepth, aRv);
+    mTypeOffset = 0;
+    return result;
   }
 
-  if (aVal.isString()) {
-    nsAutoJSString str;
-    if (!str.init(aCx, aVal)) {
-      IDB_REPORT_INTERNAL_ERR();
-      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
-    }
-    return EncodeString(str, aTypeOffset);
+  void EndSubkeyList() const { mKey.mBuffer.Append(eTerminator + mTypeOffset); }
+
+ private:
+  Key& mKey;
+  uint8_t mTypeOffset;
+  uint16_t mRecursionDepth;
+};
+
+// Implements the following algorithm:
+// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeJSValInternal(
+    JSContext* const aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
+    const uint16_t aRecursionDepth, ErrorResult& aRv) {
+  static_assert(eMaxType * kMaxArrayCollapse < 256, "Unable to encode jsvals.");
+
+  // 1. If `seen` was not given, let `seen` be a new empty set.
+  // 2. If `input` is in `seen` return invalid.
+  // Note: we replace this check with a simple recursion depth check.
+  if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) {
+    return Invalid;
   }
 
+  // 3. Jump to the appropriate step below:
+  // Note: some cases appear out of order to make the implementation more
+  //       straightforward. This shouldn't affect observable behavior.
+
+  // If Type(`input`) is Number
   if (aVal.isNumber()) {
-    double d = aVal.toNumber();
-    if (mozilla::IsNaN(d)) {
-      return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
+    const auto number = aVal.toNumber();
+
+    // 1. If `input` is NaN then return invalid.
+    if (mozilla::IsNaN(number)) {
+      return Invalid;
     }
-    EncodeNumber(d, eFloat + aTypeOffset);
-    return NS_OK;
+
+    // 2. Otherwise, return a new key with type `number` and value `input`.
+    EncodeNumber(number, eFloat + aTypeOffset);
+    return Ok();
+  }
+
+  // If Type(`input`) is String
+  if (aVal.isString()) {
+    // 1. Return a new key with type `string` and value `input`.
+    nsAutoJSString string;
+    if (!string.init(aCx, aVal)) {
+      IDB_REPORT_INTERNAL_ERR();
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+      return Exception;
+    }
+    return EncodeString(string, aTypeOffset, aRv);
   }
 
   if (aVal.isObject()) {
-    JS::Rooted<JSObject*> obj(aCx, &aVal.toObject());
+    JS::RootedObject object(aCx, &aVal.toObject());
 
-    js::ESClass cls;
-    if (!js::GetBuiltinClass(aCx, obj, &cls)) {
+    js::ESClass builtinClass;
+    if (!js::GetBuiltinClass(aCx, object, &builtinClass)) {
       IDB_REPORT_INTERNAL_ERR();
-      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+      return Exception;
     }
-    if (cls == js::ESClass::Array) {
-      aTypeOffset += eMaxType;
 
-      if (aTypeOffset == eMaxType * kMaxArrayCollapse) {
-        mBuffer.Append(aTypeOffset);
-        aTypeOffset = 0;
-      }
-      NS_ASSERTION((aTypeOffset % eMaxType) == 0 &&
-                       aTypeOffset < (eMaxType * kMaxArrayCollapse),
-                   "Wrong typeoffset");
-
-      uint32_t length;
-      if (!JS_GetArrayLength(aCx, obj, &length)) {
+    // If `input` is a Date (has a [[DateValue]] internal slot)
+    if (builtinClass == js::ESClass::Date) {
+      // 1. Let `ms` be the value of `input`’s [[DateValue]] internal slot.
+      double ms;
+      if (!js::DateGetMsecSinceEpoch(aCx, object, &ms)) {
         IDB_REPORT_INTERNAL_ERR();
-        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        return Exception;
       }
 
-      for (uint32_t index = 0; index < length; index++) {
-        JS::Rooted<JS::Value> val(aCx);
-        if (!JS_GetElement(aCx, obj, index, &val)) {
-          IDB_REPORT_INTERNAL_ERR();
-          return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
-        }
-
-        nsresult rv =
-            EncodeJSValInternal(aCx, val, aTypeOffset, aRecursionDepth + 1);
-        if (NS_FAILED(rv)) {
-          return rv;
-        }
-
-        aTypeOffset = 0;
+      // 2. If `ms` is NaN then return invalid.
+      if (mozilla::IsNaN(ms)) {
+        return Invalid;
       }
 
-      mBuffer.Append(eTerminator + aTypeOffset);
-
-      return NS_OK;
+      // 3. Otherwise, return a new key with type `date` and value `ms`.
+      EncodeNumber(ms, eDate + aTypeOffset);
+      return Ok();
     }
 
-    if (cls == js::ESClass::Date) {
-      bool valid;
-      if (!js::DateIsValid(aCx, obj, &valid)) {
-        IDB_REPORT_INTERNAL_ERR();
-        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
-      }
-      if (!valid) {
-        return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
-      }
-      double t;
-      if (!js::DateGetMsecSinceEpoch(aCx, obj, &t)) {
-        IDB_REPORT_INTERNAL_ERR();
-        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
-      }
-      EncodeNumber(t, eDate + aTypeOffset);
-      return NS_OK;
+    // If `input` is a buffer source type
+    if (JS_IsArrayBufferObject(object) || JS_IsArrayBufferViewObject(object)) {
+      const bool isViewObject = JS_IsArrayBufferViewObject(object);
+      return EncodeBinary(object, isViewObject, aTypeOffset, aRv);
     }
 
-    if (JS_IsArrayBufferObject(obj)) {
-      return EncodeBinary(obj, /* aIsViewObject */ false, aTypeOffset);
-    }
-
-    if (JS_IsArrayBufferViewObject(obj)) {
-      return EncodeBinary(obj, /* aIsViewObject */ true, aTypeOffset);
+    // If IsArray(`input`)
+    if (builtinClass == js::ESClass::Array) {
+      ArrayValueEncoder encoder(*this, aTypeOffset, aRecursionDepth);
+      return ConvertArrayValueToKey(aCx, object, encoder, aRv);
     }
   }
 
-  return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
+  // Otherwise
+  // Return invalid.
+  return Invalid;
 }
 
 // static
 nsresult Key::DecodeJSValInternal(const unsigned char*& aPos,
                                   const unsigned char* aEnd, JSContext* aCx,
                                   uint8_t aTypeOffset,
                                   JS::MutableHandle<JS::Value> aVal,
                                   uint16_t aRecursionDepth) {
@@ -391,39 +435,45 @@ nsresult Key::DecodeJSValInternal(const 
 
 #define ONE_BYTE_LIMIT 0x7E
 #define TWO_BYTE_LIMIT (0x3FFF + 0x7F)
 
 #define ONE_BYTE_ADJUST 1
 #define TWO_BYTE_ADJUST (-0x7F)
 #define THREE_BYTE_SHIFT 6
 
-nsresult Key::EncodeJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                          uint8_t aTypeOffset) {
-  return EncodeJSValInternal(aCx, aVal, aTypeOffset, 0);
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeJSVal(
+    JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
+    ErrorResult& aRv) {
+  return EncodeJSValInternal(aCx, aVal, aTypeOffset, 0, aRv);
 }
 
-nsresult Key::EncodeString(const nsAString& aString, uint8_t aTypeOffset) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeString(
+    const nsAString& aString, uint8_t aTypeOffset, ErrorResult& aRv) {
   const char16_t* start = aString.BeginReading();
   const char16_t* end = aString.EndReading();
-  return EncodeString(start, end, aTypeOffset);
+  return EncodeString(start, end, aTypeOffset, aRv);
 }
 
 template <typename T>
-nsresult Key::EncodeString(const T* aStart, const T* aEnd,
-                           uint8_t aTypeOffset) {
-  return EncodeAsString(aStart, aEnd, eString + aTypeOffset);
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeString(const T* aStart,
+                                                            const T* aEnd,
+                                                            uint8_t aTypeOffset,
+                                                            ErrorResult& aRv) {
+  return EncodeAsString(aStart, aEnd, eString + aTypeOffset, aRv);
 }
 
 template <typename T>
-nsresult Key::EncodeAsString(const T* aStart, const T* aEnd, uint8_t aType) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeAsString(
+    const T* aStart, const T* aEnd, uint8_t aType, ErrorResult& aRv) {
   // First measure how long the encoded string will be.
   if (NS_WARN_IF(aStart > aEnd || UINT32_MAX - 2 < uintptr_t(aEnd - aStart))) {
     IDB_REPORT_INTERNAL_ERR();
-    return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+    return Exception;
   }
 
   // The +2 is for initial 3 and trailing 0. We'll compensate for multi-byte
   // chars below.
   uint32_t checkedSize = aEnd - aStart;
   CheckedUint32 size = checkedSize;
   size += 2;
 
@@ -431,34 +481,37 @@ nsresult Key::EncodeAsString(const T* aS
 
   const T* start = aStart;
   const T* end = aEnd;
   for (const T* iter = start; iter < end; ++iter) {
     if (*iter > ONE_BYTE_LIMIT) {
       size += char16_t(*iter) > TWO_BYTE_LIMIT ? 2 : 1;
       if (!size.isValid()) {
         IDB_REPORT_INTERNAL_ERR();
-        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        return Exception;
       }
     }
   }
 
   // Allocate memory for the new size
   uint32_t oldLen = mBuffer.Length();
   size += oldLen;
 
   if (!size.isValid()) {
     IDB_REPORT_INTERNAL_ERR();
-    return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+    return Exception;
   }
 
   char* buffer;
   if (!mBuffer.GetMutableData(&buffer, size.value())) {
     IDB_REPORT_INTERNAL_ERR();
-    return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+    return Exception;
   }
   buffer += oldLen;
 
   // Write type marker
   *(buffer++) = aType;
 
   // Encode string
   for (const T* iter = start; iter < end; ++iter) {
@@ -476,51 +529,53 @@ nsresult Key::EncodeAsString(const T* aS
     }
   }
 
   // Write end marker
   *(buffer++) = eTerminator;
 
   NS_ASSERTION(buffer == mBuffer.EndReading(), "Wrote wrong number of bytes");
 
-  return NS_OK;
+  return indexedDB::Ok();
 }
 
-nsresult Key::EncodeLocaleString(const nsDependentString& aString,
-                                 uint8_t aTypeOffset,
-                                 const nsCString& aLocale) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeLocaleString(
+    const nsDependentString& aString, uint8_t aTypeOffset,
+    const nsCString& aLocale, ErrorResult& aRv) {
   const int length = aString.Length();
   if (length == 0) {
-    return NS_OK;
+    return Ok();
   }
   const UChar* ustr = reinterpret_cast<const UChar*>(aString.BeginReading());
 
   UErrorCode uerror = U_ZERO_ERROR;
   UCollator* collator = ucol_open(aLocale.get(), &uerror);
   if (NS_WARN_IF(U_FAILURE(uerror))) {
-    return NS_ERROR_FAILURE;
+    aRv.Throw(NS_ERROR_FAILURE);
+    return Exception;
   }
   MOZ_ASSERT(collator);
 
   AutoTArray<uint8_t, 128> keyBuffer;
   int32_t sortKeyLength = ucol_getSortKey(
       collator, ustr, length, keyBuffer.Elements(), keyBuffer.Length());
   if (sortKeyLength > (int32_t)keyBuffer.Length()) {
     keyBuffer.SetLength(sortKeyLength);
     sortKeyLength = ucol_getSortKey(collator, ustr, length,
                                     keyBuffer.Elements(), sortKeyLength);
   }
 
   ucol_close(collator);
   if (NS_WARN_IF(sortKeyLength == 0)) {
-    return NS_ERROR_FAILURE;
+    aRv.Throw(NS_ERROR_FAILURE);
+    return Exception;
   }
 
   return EncodeString(keyBuffer.Elements(),
-                      keyBuffer.Elements() + sortKeyLength, aTypeOffset);
+                      keyBuffer.Elements() + sortKeyLength, aTypeOffset, aRv);
 }
 
 // static
 nsresult Key::DecodeJSVal(const unsigned char*& aPos, const unsigned char* aEnd,
                           JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
   return DecodeJSValInternal(aPos, aEnd, aCx, 0, aVal, 0);
 }
 
@@ -618,32 +673,34 @@ double Key::DecodeNumber(const unsigned 
   // Note: The subtraction from 0 below is necessary to fix
   // MSVC build warning C4146 (negating an unsigned value).
   const uint64_t signbit = FloatingPoint<double>::kSignBit;
   uint64_t bits = number & signbit ? (number & ~signbit) : (0 - number);
 
   return BitwiseCast<double>(bits);
 }
 
-nsresult Key::EncodeBinary(JSObject* aObject, bool aIsViewObject,
-                           uint8_t aTypeOffset) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::EncodeBinary(JSObject* aObject,
+                                                            bool aIsViewObject,
+                                                            uint8_t aTypeOffset,
+                                                            ErrorResult& aRv) {
   uint8_t* bufferData;
   uint32_t bufferLength;
   bool unused;
 
   if (aIsViewObject) {
     js::GetArrayBufferViewLengthAndData(aObject, &bufferLength, &unused,
                                         &bufferData);
   } else {
     js::GetArrayBufferLengthAndData(aObject, &bufferLength, &unused,
                                     &bufferData);
   }
 
   return EncodeAsString(bufferData, bufferData + bufferLength,
-                        eBinary + aTypeOffset);
+                        eBinary + aTypeOffset, aRv);
 }
 
 // static
 JSObject* Key::DecodeBinary(const unsigned char*& aPos,
                             const unsigned char* aEnd, JSContext* aCx) {
   MOZ_ASSERT(*aPos % eMaxType == eBinary, "Don't call me!");
 
   const unsigned char* buffer = ++aPos;
@@ -716,31 +773,32 @@ nsresult Key::SetFromStatement(mozIStora
   return SetFromSource(aStatement, aIndex);
 }
 
 nsresult Key::SetFromValueArray(mozIStorageValueArray* aValues,
                                 uint32_t aIndex) {
   return SetFromSource(aValues, aIndex);
 }
 
-void Key::SetFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                       ErrorResult& aRv) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::SetFromJSVal(
+    JSContext* aCx, JS::Handle<JS::Value> aVal, ErrorResult& aRv) {
   mBuffer.Truncate();
 
   if (aVal.isNull() || aVal.isUndefined()) {
     Unset();
-    return;
+    return Ok();
   }
 
-  aRv = EncodeJSVal(aCx, aVal, 0);
-  if (aRv.Failed()) {
+  auto result = EncodeJSVal(aCx, aVal, 0, aRv);
+  if (!result.Is(Ok, aRv)) {
     Unset();
-    return;
+    return result;
   }
   TrimBuffer();
+  return Ok();
 }
 
 nsresult Key::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const {
   if (IsUnset()) {
     aVal.setUndefined();
     return NS_OK;
   }
 
@@ -759,25 +817,24 @@ nsresult Key::ToJSVal(JSContext* aCx, JS
   JS::Rooted<JS::Value> value(aCx);
   nsresult rv = ToJSVal(aCx, &value);
   if (NS_SUCCEEDED(rv)) {
     aVal = value;
   }
   return rv;
 }
 
-nsresult Key::AppendItem(JSContext* aCx, bool aFirstOfArray,
-                         JS::Handle<JS::Value> aVal) {
-  nsresult rv = EncodeJSVal(aCx, aVal, aFirstOfArray ? eMaxType : 0);
-  if (NS_FAILED(rv)) {
+IDBResult<void, IDBSpecialValue::Invalid> Key::AppendItem(
+    JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal,
+    ErrorResult& aRv) {
+  auto result = EncodeJSVal(aCx, aVal, aFirstOfArray ? eMaxType : 0, aRv);
+  if (!result.Is(Ok, aRv)) {
     Unset();
-    return rv;
   }
-
-  return NS_OK;
+  return result;
 }
 
 template <typename T>
 nsresult Key::SetFromSource(T* aSource, uint32_t aIndex) {
   const uint8_t* data;
   uint32_t dataLength = 0;
 
   nsresult rv = aSource->GetSharedBlob(aIndex, &dataLength, &data);
--- a/dom/indexedDB/Key.h
+++ b/dom/indexedDB/Key.h
@@ -2,33 +2,34 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_indexeddb_key_h__
 #define mozilla_dom_indexeddb_key_h__
 
+#include "mozilla/dom/indexedDB/IDBResult.h"
+
 #include "js/RootingAPI.h"
+#include "jsapi.h"
+#include "mozilla/ErrorResult.h"
 #include "nsString.h"
 
 class mozIStorageStatement;
 class mozIStorageValueArray;
 
 namespace IPC {
 
 template <typename>
 struct ParamTraits;
 
 }  // namespace IPC
 
 namespace mozilla {
-
-class ErrorResult;
-
 namespace dom {
 namespace indexedDB {
 
 class Key {
   friend struct IPC::ParamTraits<Key>;
 
   nsCString mBuffer;
 
@@ -45,21 +46,16 @@ class Key {
 
   static const uint8_t kMaxArrayCollapse = uint8_t(3);
   static const uint8_t kMaxRecursionDepth = uint8_t(64);
 
   Key() { Unset(); }
 
   explicit Key(const nsACString& aBuffer) : mBuffer(aBuffer) {}
 
-  Key& operator=(const nsAString& aString) {
-    SetFromString(aString);
-    return *this;
-  }
-
   Key& operator=(int64_t aInt) {
     SetFromInteger(aInt);
     return *this;
   }
 
   bool operator==(const Key& aOther) const {
     Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid());
 
@@ -128,39 +124,42 @@ class Key {
 
   void ToString(nsString& aString) const {
     Assert(IsString());
     const unsigned char* pos = BufferStart();
     DecodeString(pos, BufferEnd(), aString);
     Assert(pos >= BufferEnd());
   }
 
-  void SetFromString(const nsAString& aString) {
-    mBuffer.Truncate();
-    EncodeString(aString, 0);
-    TrimBuffer();
-  }
+  IDBResult<void, IDBSpecialValue::Invalid> SetFromString(
+      const nsAString& aString, ErrorResult& aRv);
 
   void SetFromInteger(int64_t aInt) {
     mBuffer.Truncate();
     EncodeNumber(double(aInt), eFloat);
     TrimBuffer();
   }
 
-  void SetFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                    ErrorResult& aRv);
+  // This function implements the standard algorithm "convert a value to a key".
+  // A key return value is indicated by returning `true` whereas `false` means
+  // either invalid (if `aRv.Failed()` is `false`) or an exception (otherwise).
+  IDBResult<void, IDBSpecialValue::Invalid> SetFromJSVal(
+      JSContext* aCx, JS::Handle<JS::Value> aVal, ErrorResult& aRv);
 
   nsresult ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const;
 
   nsresult ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aVal) const;
 
-  nsresult AppendItem(JSContext* aCx, bool aFirstOfArray,
-                      JS::Handle<JS::Value> aVal);
+  // See SetFromJSVal() for the meaning of values returned by this function.
+  IDBResult<void, IDBSpecialValue::Invalid> AppendItem(
+      JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal,
+      ErrorResult& aRv);
 
-  nsresult ToLocaleBasedKey(Key& aTarget, const nsCString& aLocale) const;
+  IDBResult<void, IDBSpecialValue::Invalid> ToLocaleBasedKey(
+      Key& aTarget, const nsCString& aLocale, ErrorResult& aRv) const;
 
   void FinishArray() { TrimBuffer(); }
 
   const nsCString& GetBuffer() const { return mBuffer; }
 
   nsresult BindToStatement(mozIStorageStatement* aStatement,
                            const nsACString& aParamName) const;
 
@@ -177,17 +176,87 @@ class Key {
 
     if (result > 0) {
       return 1;
     }
 
     return 0;
   }
 
+  // Implementation of the array branch of step 3 of
+  // https://w3c.github.io/IndexedDB/#convert-value-to-key
+  template <typename ArrayConversionPolicy>
+  static IDBResult<void, IDBSpecialValue::Invalid> ConvertArrayValueToKey(
+      JSContext* const aCx, JS::HandleObject aObject,
+      ArrayConversionPolicy&& aPolicy, ErrorResult& aRv) {
+    // 1. Let `len` be ? ToLength( ? Get(`input`, "length")).
+    uint32_t len;
+    if (!JS_GetArrayLength(aCx, aObject, &len)) {
+      aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+      return Exception;
+    }
+
+    // 2. Add `input` to `seen`.
+    aPolicy.AddToSeenSet(aCx, aObject);
+
+    // 3. Let `keys` be a new empty list.
+    aPolicy.BeginSubkeyList();
+
+    // 4. Let `index` be 0.
+    uint32_t index = 0;
+
+    // 5. While `index` is less than `len`:
+    while (index < len) {
+      JS::RootedId indexId(aCx);
+      if (!JS_IndexToId(aCx, index, &indexId)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        return Exception;
+      }
+
+      // 1. Let `hop` be ? HasOwnProperty(`input`, `index`).
+      bool hop;
+      if (!JS_HasOwnPropertyById(aCx, aObject, indexId, &hop)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        return Exception;
+      }
+
+      // 2. If `hop` is false, return invalid.
+      if (!hop) {
+        return Invalid;
+      }
+
+      // 3. Let `entry` be ? Get(`input`, `index`).
+      JS::RootedValue entry(aCx);
+      if (!JS_GetPropertyById(aCx, aObject, indexId, &entry)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        return Exception;
+      }
+
+      // 4. Let `key` be the result of running the steps to convert a value to a
+      //    key with arguments `entry` and `seen`.
+      // 5. ReturnIfAbrupt(`key`).
+      // 6. If `key` is invalid abort these steps and return invalid.
+      // 7. Append `key` to `keys`.
+      auto result = aPolicy.ConvertSubkey(aCx, entry, index, aRv);
+      if (!result.Is(Ok, aRv)) {
+        return result;
+      }
+
+      // 8. Increase `index` by 1.
+      index += 1;
+    }
+
+    // 6. Return a new array key with value `keys`.
+    aPolicy.EndSubkeyList();
+    return Ok();
+  }
+
  private:
+  class MOZ_STACK_CLASS ArrayValueEncoder;
+
   const unsigned char* BufferStart() const {
     return reinterpret_cast<const unsigned char*>(mBuffer.BeginReading());
   }
 
   const unsigned char* BufferEnd() const {
     return reinterpret_cast<const unsigned char*>(mBuffer.EndReading());
   }
 
@@ -198,52 +267,64 @@ class Key {
     while (!*end) {
       --end;
     }
 
     mBuffer.Truncate(end + 1 - mBuffer.BeginReading());
   }
 
   // Encoding functions. These append the encoded value to the end of mBuffer
-  nsresult EncodeJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                       uint8_t aTypeOffset);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeJSVal(
+      JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
+      ErrorResult& aRv);
 
-  nsresult EncodeString(const nsAString& aString, uint8_t aTypeOffset);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeString(
+      const nsAString& aString, uint8_t aTypeOffset, ErrorResult& aRv);
 
   template <typename T>
-  nsresult EncodeString(const T* aStart, const T* aEnd, uint8_t aTypeOffset);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeString(const T* aStart,
+                                                         const T* aEnd,
+                                                         uint8_t aTypeOffset,
+                                                         ErrorResult& aRv);
 
   template <typename T>
-  nsresult EncodeAsString(const T* aStart, const T* aEnd, uint8_t aType);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeAsString(const T* aStart,
+                                                           const T* aEnd,
+                                                           uint8_t aType,
+                                                           ErrorResult& aRv);
 
-  nsresult EncodeLocaleString(const nsDependentString& aString,
-                              uint8_t aTypeOffset, const nsCString& aLocale);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeLocaleString(
+      const nsDependentString& aString, uint8_t aTypeOffset,
+      const nsCString& aLocale, ErrorResult& aRv);
 
   void EncodeNumber(double aFloat, uint8_t aType);
 
-  nsresult EncodeBinary(JSObject* aObject, bool aIsViewObject,
-                        uint8_t aTypeOffset);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeBinary(JSObject* aObject,
+                                                         bool aIsViewObject,
+                                                         uint8_t aTypeOffset,
+                                                         ErrorResult& aRv);
 
   // Decoding functions. aPos points into mBuffer and is adjusted to point
   // past the consumed value.
   static nsresult DecodeJSVal(const unsigned char*& aPos,
                               const unsigned char* aEnd, JSContext* aCx,
                               JS::MutableHandle<JS::Value> aVal);
 
   static void DecodeString(const unsigned char*& aPos,
                            const unsigned char* aEnd, nsString& aString);
 
   static double DecodeNumber(const unsigned char*& aPos,
                              const unsigned char* aEnd);
 
   static JSObject* DecodeBinary(const unsigned char*& aPos,
                                 const unsigned char* aEnd, JSContext* aCx);
 
-  nsresult EncodeJSValInternal(JSContext* aCx, JS::Handle<JS::Value> aVal,
-                               uint8_t aTypeOffset, uint16_t aRecursionDepth);
+  IDBResult<void, IDBSpecialValue::Invalid> EncodeJSValInternal(
+      JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
+      uint16_t aRecursionDepth, ErrorResult& aRv);
 
   static nsresult DecodeJSValInternal(const unsigned char*& aPos,
                                       const unsigned char* aEnd, JSContext* aCx,
                                       uint8_t aTypeOffset,
                                       JS::MutableHandle<JS::Value> aVal,
                                       uint16_t aRecursionDepth);
 
   template <typename T>
--- a/dom/indexedDB/KeyPath.cpp
+++ b/dom/indexedDB/KeyPath.cpp
@@ -351,18 +351,21 @@ nsresult KeyPath::ExtractKey(JSContext* 
   for (uint32_t i = 0; i < len; ++i) {
     nsresult rv =
         GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
                                   DoNotCreateProperties, nullptr, nullptr);
     if (NS_FAILED(rv)) {
       return rv;
     }
 
-    if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) {
+    ErrorResult errorResult;
+    auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value, errorResult);
+    if (!result.Is(Ok, errorResult)) {
       NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
+      errorResult.SuppressException();
       return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
     }
   }
 
   aKey.FinishArray();
 
   return NS_OK;
 }
@@ -413,18 +416,21 @@ nsresult KeyPath::ExtractOrCreateKey(JSC
 
   nsresult rv =
       GetJSValFromKeyPathString(aCx, aValue, mStrings[0], value.address(),
                                 CreateProperties, aCallback, aClosure);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
-  if (NS_FAILED(aKey.AppendItem(aCx, false, value))) {
+  ErrorResult errorResult;
+  auto result = aKey.AppendItem(aCx, false, value, errorResult);
+  if (!result.Is(Ok, errorResult)) {
     NS_ASSERTION(aKey.IsUnset(), "Should be unset");
+    errorResult.SuppressException();
     return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
   }
 
   aKey.FinishArray();
 
   return NS_OK;
 }
 
--- a/dom/indexedDB/moz.build
+++ b/dom/indexedDB/moz.build
@@ -16,16 +16,18 @@ BROWSER_CHROME_MANIFESTS += ['test/brows
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/xpcshell-child-process.ini',
     'test/unit/xpcshell-parent-process.ini'
 ]
 
+TEST_DIRS += ['test/gtest']
+
 EXPORTS.mozilla.dom += [
     'IDBCursor.h',
     'IDBDatabase.h',
     'IDBEvents.h',
     'IDBFactory.h',
     'IDBFileHandle.h',
     'IDBFileRequest.h',
     'IDBIndex.h',
@@ -37,16 +39,17 @@ EXPORTS.mozilla.dom += [
     'IDBWrapperCache.h',
     'IndexedDatabase.h',
     'IndexedDatabaseManager.h',
 ]
 
 EXPORTS.mozilla.dom.indexedDB += [
     'ActorsParent.h',
     'FileSnapshot.h',
+    'IDBResult.h',
     'Key.h',
     'KeyPath.h',
     'SerializationHelpers.h',
 ]
 
 UNIFIED_SOURCES += [
     'ActorsChild.cpp',
     'FileInfo.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/gtest/TestIDBResult.cpp
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IDBResult.h"
+
+#include "gtest/gtest.h"
+
+using mozilla::ErrorResult;
+using namespace mozilla::dom::indexedDB;
+
+TEST(IDBResultTest, ConstructWithValue)
+{
+  ErrorResult rv;
+  IDBResult<int, IDBSpecialValue::Failure> result(Ok(0));
+  EXPECT_FALSE(result.Is(Failure, rv));
+  EXPECT_TRUE(result.Is(Ok, rv));
+  EXPECT_EQ(result.Unwrap(rv), 0);
+}
+
+TEST(IDBResultTest, Expand)
+{
+  ErrorResult rv;
+  IDBResult<int, IDBSpecialValue::Failure> narrow{Failure};
+  IDBResult<int, IDBSpecialValue::Failure, IDBSpecialValue::Invalid> wide{
+      narrow};
+  EXPECT_TRUE(wide.Is(Failure, rv));
+}
+
+IDBResult<int, IDBSpecialValue::Failure> ThrowException(ErrorResult& aRv) {
+  aRv.Throw(NS_ERROR_FAILURE);
+  return Exception;
+}
+
+TEST(IDBResultTest, ThrowException)
+{
+  ErrorResult rv;
+  const auto result = ThrowException(rv);
+  EXPECT_TRUE(result.Is(Exception, rv));
+  rv.SuppressException();
+}
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/gtest/moz.build
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+    'TestIDBResult.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul-gtest'
+
+LOCAL_INCLUDES += [
+    '/dom/indexedDB',
+]