Repository: parse-community/Parse-SDK-JS
Pull request #2445 (2025-02-12 19:34:33)

fix: Remove validation error handler option `error` from various methods of `Parse.Object`
## Pull Request

- Report security issues [confidentially](https://github.com/parse-community/Parse-SDK-JS/security/policy).
- Any contribution is under this [license](https://github.com/parse-community/Parse-SDK-JS/blob/alpha/LICENSE).
- Link this pull request to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

## Issue
<!-- Add the link to the issue that this PR closes. -->
You can pass an option in `set` to handle validation errors. The problem is this option isn't well documented and isn't widely used. This method of error handling is called backbone and was removed in 2.0.0. There are two errors (invalid key name and invalid acl) that are being checked. This could potentially be a breaking change if a user is setting invalid fields and never saving as saving would throw the errors.

```
object.set('$$$', 'o_O', {
  error: function (thisObj, err) {
    // err is ParseError.INVALID_KEY_NAME but doesn't have an error message.
  },
});
```
Closes: https://github.com/parse-community/Parse-SDK-JS/issues/2104

## Approach
<!-- Describe the changes in this PR. -->
* Remove backbone `options.error`
* Add error message for `Parse.Error.INVALID_KEY_NAME`
* Improve documentation for chaining
* `set` now throws validation errors

## Tasks
<!-- Delete tasks that don't apply. -->

- [x] Add tests
- [x] Add changes to documentation (guides, repository pages, code comments)


parse-github-assistant[bot]: 
### Thanks for opening this pull request!

<!-- parse-issue-bot-meta-tag-id {"suggestPr":false,"thanksFeature":false,"thanksPr":false} -->


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/pull/2445?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`d5b4229`)](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/commit/d5b422939818df432db03b89a615f5e9f594da24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) to head [(`93b31e5`)](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/commit/93b31e533da42164eb746d0e97e76b443387638d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).
> Report is 2 commits behind head on alpha.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             alpha     #2445   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         6271      6271           
  Branches      1480      1478    -2     
=========================================
  Hits          6271      6271           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/pull/2445?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).

mtrezza: Now is a good time to merge any breaking changes. We can release with Parse Server 8.
mtrezza: BREAKING CHANGE: Removes `Parse.Object.set` option `error` for validation error handling and instead throws an error on validation errors.
dplewis: There is also `Parse.Object.unset` and `Parse.Role.setName` that throw errors now.
mtrezza: And why are we making this change in behavior, instead of throwing at save? 
dplewis: > throw not at save but at set et al.

The error would get thrown before you get a chance to save.
mtrezza: So the original behavior is:

- if error option is set, then error is thrown at .set
- if error option is not set, then error is thrown at .save

New behavior with this PR:

- error is thrown at .set (error option doesn't exist anymore)

Is that correct?
dplewis: This is correct. The originals idea for this PR was to catch invalid ACL in some cases users have reported that save didn’t throw at all
mtrezza: Do you think it makes more sense to throw on .set or on .save? Throwing on .save seems to make more sense to me, because .set seems more like a non-op. Do we have any other similar examples in the SDK?
dplewis: I think you are right, I didn't find anything in the other SDKS
mtrezza: BREAKING CHANGE: Removes the error handler option `error` from `Parse.Object.set`, `Parse.Object.setACL`, `Parse.Object.unset`, `Parse.Role.setName` and instead throws on validation error. Previously, if the `error` option was set, the handler was invoked if a validation error occurred on `Parse.Object.set`, and if no handler was set, an error was thrown on `Parse.Object.save`. The new behavior is that an error is thrown at `Parse.Object.set`. For example, instead of using `Parse.Object.set(key, value, { error: ... })` wrap `Parse.Object.set(key, value)` into a `try/catch` block. 

@dplewis Is this proposed changelog entry correct?
dplewis: LGTM!

Patch:
diff --git a/src/ParseInstallation.ts b/src/ParseInstallation.ts
index fa69e29..465d591 100644
--- a/src/ParseInstallation.ts
+++ b/src/ParseInstallation.ts
@@ -40,7 +40,9 @@ class ParseInstallation extends ParseObject {
   constructor(attributes?: AttributeMap) {
     super('_Installation');
     if (attributes && typeof attributes === 'object') {
-      if (!this.set(attributes)) {
+      try {
+        this.set(attributes || {});
+      } catch (_) {
         throw new Error("Can't create an invalid Installation");
       }
     }
diff --git a/src/ParseObject.ts b/src/ParseObject.ts
index bd3d6f8..c539307 100644
--- a/src/ParseObject.ts
+++ b/src/ParseObject.ts
@@ -136,8 +136,12 @@ class ParseObject {
         options = attributes as any;
       }
     }
-    if (toSet && !this.set(toSet, options)) {
-      throw new Error("Can't create an invalid Parse Object");
+    if (toSet) {
+      try {
+        this.set(toSet, options);
+      } catch (_) {
+        throw new Error("Can't create an invalid Parse Object");
+      }
     }
   }
 
@@ -733,9 +737,9 @@ class ParseObject {
    * @param {(string|object)} value The value to give it.
    * @param {object} options A set of options for the set.
    *     The only supported option is <code>error</code>.
-   * @returns {(ParseObject|boolean)} true if the set succeeded.
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  set(key: any, value?: any, options?: any): ParseObject | boolean {
+  set(key: any, value?: any, options?: any): this {
     let changes = {};
     const newOps = {};
     if (key && typeof key === 'object') {
@@ -804,12 +808,9 @@ class ParseObject {
 
     // Validate changes
     if (!options.ignoreValidation) {
-      const validation = this.validate(newValues);
-      if (validation) {
-        if (typeof options.error === 'function') {
-          options.error(this, validation);
-        }
-        return false;
+      const validationError = this.validate(newValues);
+      if (validationError) {
+        throw validationError;
       }
     }
 
@@ -831,9 +832,9 @@ class ParseObject {
    *
    * @param {string} attr The string name of an attribute.
    * @param options
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  unset(attr: string, options?: { [opt: string]: any }): ParseObject | boolean {
+  unset(attr: string, options?: { [opt: string]: any }): this {
     options = options || {};
     options.unset = true;
     return this.set(attr, null, options);
@@ -845,9 +846,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param amount {Number} The amount to increment by (optional).
-   * @returns {(ParseObject|boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  increment(attr: string, amount?: number): ParseObject | boolean {
+  increment(attr: string, amount?: number): this {
     if (typeof amount === 'undefined') {
       amount = 1;
     }
@@ -863,9 +864,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param amount {Number} The amount to decrement by (optional).
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  decrement(attr: string, amount?: number): ParseObject | boolean {
+  decrement(attr: string, amount?: number): this {
     if (typeof amount === 'undefined') {
       amount = 1;
     }
@@ -881,9 +882,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param item {} The item to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  add(attr: string, item: any): ParseObject | boolean {
+  add(attr: string, item: any): this {
     return this.set(attr, new AddOp([item]));
   }
 
@@ -893,9 +894,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param items {Object[]} The items to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addAll(attr: string, items: Array<any>): ParseObject | boolean {
+  addAll(attr: string, items: Array<any>): this {
     return this.set(attr, new AddOp(items));
   }
 
@@ -906,9 +907,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param item {} The object to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addUnique(attr: string, item: any): ParseObject | boolean {
+  addUnique(attr: string, item: any): this {
     return this.set(attr, new AddUniqueOp([item]));
   }
 
@@ -919,9 +920,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param items {Object[]} The objects to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addAllUnique(attr: string, items: Array<any>): ParseObject | boolean {
+  addAllUnique(attr: string, items: Array<any>): this {
     return this.set(attr, new AddUniqueOp(items));
   }
 
@@ -931,9 +932,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param item {} The object to remove.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  remove(attr: string, item: any): ParseObject | boolean {
+  remove(attr: string, item: any): this {
     return this.set(attr, new RemoveOp([item]));
   }
 
@@ -943,9 +944,9 @@ class ParseObject {
    *
    * @param attr {String} The key.
    * @param items {Object[]} The object to remove.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  removeAll(attr: string, items: Array<any>): ParseObject | boolean {
+  removeAll(attr: string, items: Array<any>): this {
     return this.set(attr, new RemoveOp(items));
   }
 
@@ -1099,7 +1100,7 @@ class ParseObject {
     }
     for (const key in attrs) {
       if (!/^[A-Za-z][0-9A-Za-z_.]*$/.test(key)) {
-        return new ParseError(ParseError.INVALID_KEY_NAME);
+        return new ParseError(ParseError.INVALID_KEY_NAME, `Invalid key name: "${key}"`);
       }
     }
     return false;
@@ -1124,10 +1125,10 @@ class ParseObject {
    *
    * @param {Parse.ACL} acl An instance of Parse.ACL.
    * @param {object} options
-   * @returns {(ParseObject | boolean)} Whether the set passed validation.
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    * @see Parse.Object#set
    */
-  setACL(acl: ParseACL, options?: any): ParseObject | boolean {
+  setACL(acl: ParseACL, options?: any): this {
     return this.set('ACL', acl, options);
   }
 
@@ -1154,9 +1155,9 @@ class ParseObject {
   /**
    * Clears all attributes on a model
    *
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  clear(): ParseObject | boolean {
+  clear(): this {
     const attributes = this.attributes;
     const erasable = {};
     let readonly = ['createdAt', 'updatedAt'];
@@ -1320,7 +1321,7 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the save
    * completes.
    */
-  save(
+  async save(
     arg1: undefined | string | { [attr: string]: any } | null,
     arg2: SaveOptions | any,
     arg3?: SaveOptions
@@ -1337,17 +1338,9 @@ class ParseObject {
       attrs[arg1] = arg2;
       options = arg3;
     }
-
     options = options || {};
     if (attrs) {
-      let validationError;
-      options.error = (_, validation) => {
-        validationError = validation;
-      };
-      const success = this.set(attrs, options);
-      if (!success) {
-        return Promise.reject(validationError);
-      }
+      this.set(attrs, options);
     }
     const saveOptions = ParseObject._getRequestOptions(options);
     const controller = CoreManager.getObjectController();
@@ -1985,7 +1978,9 @@ class ParseObject {
       }
 
       if (attributes && typeof attributes === 'object') {
-        if (!this.set(attributes || {}, options)) {
+        try {
+          this.set(attributes || {}, options);
+        } catch (_) {
           throw new Error("Can't create an invalid Parse Object");
         }
       }
diff --git a/src/ParseSession.ts b/src/ParseSession.ts
index b20cb4f..e783a30 100644
--- a/src/ParseSession.ts
+++ b/src/ParseSession.ts
@@ -20,7 +20,9 @@ class ParseSession extends ParseObject {
   constructor(attributes?: any) {
     super('_Session');
     if (attributes && typeof attributes === 'object') {
-      if (!this.set(attributes || {})) {
+      try {
+        this.set(attributes || {});
+      } catch (_) {
         throw new Error("Can't create an invalid Session");
       }
     }
diff --git a/src/ParseUser.ts b/src/ParseUser.ts
index 2cc4287..47be73d 100644
--- a/src/ParseUser.ts
+++ b/src/ParseUser.ts
@@ -41,7 +41,9 @@ class ParseUser extends ParseObject {
   constructor(attributes?: AttributeMap) {
     super('_User');
     if (attributes && typeof attributes === 'object') {
-      if (!this.set(attributes || {})) {
+      try {
+        this.set(attributes || {});
+      } catch (_) {
         throw new Error("Can't create an invalid Parse User");
       }
     }
diff --git a/types/ParseObject.d.ts b/types/ParseObject.d.ts
index e51b188..7b49bfc 100644
--- a/types/ParseObject.d.ts
+++ b/types/ParseObject.d.ts
@@ -241,59 +241,59 @@ declare class ParseObject {
    * @param {(string|object)} value The value to give it.
    * @param {object} options A set of options for the set.
    *     The only supported option is <code>error</code>.
-   * @returns {(ParseObject|boolean)} true if the set succeeded.
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  set(key: any, value?: any, options?: any): ParseObject | boolean;
+  set(key: any, value?: any, options?: any): this;
   /**
    * Remove an attribute from the model. This is a noop if the attribute doesn't
    * exist.
    *
    * @param {string} attr The string name of an attribute.
    * @param options
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
   unset(
     attr: string,
     options?: {
       [opt: string]: any;
     }
-  ): ParseObject | boolean;
+  ): this;
   /**
    * Atomically increments the value of the given attribute the next time the
    * object is saved. If no amount is specified, 1 is used by default.
    *
    * @param attr {String} The key.
    * @param amount {Number} The amount to increment by (optional).
-   * @returns {(ParseObject|boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  increment(attr: string, amount?: number): ParseObject | boolean;
+  increment(attr: string, amount?: number): this;
   /**
    * Atomically decrements the value of the given attribute the next time the
    * object is saved. If no amount is specified, 1 is used by default.
    *
    * @param attr {String} The key.
    * @param amount {Number} The amount to decrement by (optional).
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  decrement(attr: string, amount?: number): ParseObject | boolean;
+  decrement(attr: string, amount?: number): this;
   /**
    * Atomically add an object to the end of the array associated with a given
    * key.
    *
    * @param attr {String} The key.
    * @param item {} The item to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  add(attr: string, item: any): ParseObject | boolean;
+  add(attr: string, item: any): this;
   /**
    * Atomically add the objects to the end of the array associated with a given
    * key.
    *
    * @param attr {String} The key.
    * @param items {Object[]} The items to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addAll(attr: string, items: Array<any>): ParseObject | boolean;
+  addAll(attr: string, items: Array<any>): this;
   /**
    * Atomically add an object to the array associated with a given key, only
    * if it is not already present in the array. The position of the insert is
@@ -301,9 +301,9 @@ declare class ParseObject {
    *
    * @param attr {String} The key.
    * @param item {} The object to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addUnique(attr: string, item: any): ParseObject | boolean;
+  addUnique(attr: string, item: any): this;
   /**
    * Atomically add the objects to the array associated with a given key, only
    * if it is not already present in the array. The position of the insert is
@@ -311,27 +311,27 @@ declare class ParseObject {
    *
    * @param attr {String} The key.
    * @param items {Object[]} The objects to add.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  addAllUnique(attr: string, items: Array<any>): ParseObject | boolean;
+  addAllUnique(attr: string, items: Array<any>): this;
   /**
    * Atomically remove all instances of an object from the array associated
    * with a given key.
    *
    * @param attr {String} The key.
    * @param item {} The object to remove.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  remove(attr: string, item: any): ParseObject | boolean;
+  remove(attr: string, item: any): this;
   /**
    * Atomically remove all instances of the objects from the array associated
    * with a given key.
    *
    * @param attr {String} The key.
    * @param items {Object[]} The object to remove.
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  removeAll(attr: string, items: Array<any>): ParseObject | boolean;
+  removeAll(attr: string, items: Array<any>): this;
   /**
    * Returns an instance of a subclass of Parse.Op describing what kind of
    * modification has been performed on this field since the last time it was
@@ -410,10 +410,10 @@ declare class ParseObject {
    *
    * @param {Parse.ACL} acl An instance of Parse.ACL.
    * @param {object} options
-   * @returns {(ParseObject | boolean)} Whether the set passed validation.
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    * @see Parse.Object#set
    */
-  setACL(acl: ParseACL, options?: any): ParseObject | boolean;
+  setACL(acl: ParseACL, options?: any): this;
   /**
    * Clears any (or specific) changes to this object made since the last call to save()
    *
@@ -423,9 +423,9 @@ declare class ParseObject {
   /**
    * Clears all attributes on a model
    *
-   * @returns {(ParseObject | boolean)}
+   * @returns {Parse.Object} Returns the object, so you can chain this call.
    */
-  clear(): ParseObject | boolean;
+  clear(): this;
   /**
    * Fetch the model from the server. If the server's representation of the
    * model differs from its current attributes, they will be overriden.
diff --git a/types/ParseUser.d.ts b/types/ParseUser.d.ts
index 9a88c21..98295d1 100644
--- a/types/ParseUser.d.ts
+++ b/types/ParseUser.d.ts
@@ -171,7 +171,7 @@ declare class ParseUser extends ParseObject {
    * @param {string} email
    * @returns {boolean}
    */
-  setEmail(email: string): boolean | ParseObject;
+  setEmail(email: string): this;
   /**
    * Returns the session token for this user, if the user has been logged in,
    * or if it is the result of a query with the master key. Otherwise, returns


Test patch:
diff --git a/integration/test/ParseACLTest.js b/integration/test/ParseACLTest.js
index d1340cf..32f8e94 100644
--- a/integration/test/ParseACLTest.js
+++ b/integration/test/ParseACLTest.js
@@ -10,7 +10,9 @@ describe('Parse.ACL', () => {
 
   it('acl must be valid', () => {
     const user = new Parse.User();
-    assert.equal(user.setACL(`Ceci n'est pas un ACL.`), false);
+    expect(() => user.setACL(`Ceci n'est pas un ACL.`)).toThrow(
+      new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.')
+    );
   });
 
   it('can refresh object with acl', async () => {
diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index e289923..0597768 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -546,24 +546,20 @@ describe('Parse Object', () => {
     });
   });
 
-  it('cannot create invalid key names', done => {
+  it('cannot create invalid key names', async () => {
+    const error = new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: "foo^bar"`);
     const item = new Parse.Object('Item');
-    assert(!item.set({ 'foo^bar': 'baz' }));
-    item.save({ 'foo^bar': 'baz' }).catch(e => {
-      assert.equal(e.code, Parse.Error.INVALID_KEY_NAME);
-      done();
-    });
+    expect(() => {
+      item.set({ 'foo^bar': 'baz' });
+    }).toThrow(error);
+    await expectAsync(item.save({ 'foo^bar': 'baz' })).toBeRejectedWith(error);
   });
 
   it('cannot use invalid key names in multiple sets', () => {
     const item = new Parse.Object('Item');
-    assert(
-      !item.set({
-        foobar: 'baz',
-        'foo^bar': 'baz',
-      })
-    );
-    assert(!item.get('foobar'));
+    expect(() => {
+      item.set({ foobar: 'baz', 'foo^bar': 'baz' });
+    }).toThrow(new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: "foo^bar"`));
   });
 
   it('can unset fields', done => {
@@ -1135,12 +1131,43 @@ describe('Parse Object', () => {
     parent.set('children', [child1, child2]);
     parent.set('bastard', child3);
 
-    expect(parent.save).toThrow();
-    let results = await new Parse.Query(Child).find();
-    assert.equal(results.length, 0);
-
     await parent.save(null, { cascadeSave: true });
-    results = await new Parse.Query(Child).find();
+    const results = await new Parse.Query(Child).find();
+    assert.equal(results.length, 3);
+
+    parent.set('dead', true);
+    child1.set('dead', true);
+    await parent.save(null);
+    const rob = await new Parse.Query(Child).equalTo('name', 'rob').first();
+    expect(rob.get('dead')).toBe(true);
+
+    parent.set('lastname', 'stark');
+    child3.set('lastname', 'stark');
+    await parent.save(null, { cascadeSave: false });
+    const john = await new Parse.Query(Child).doesNotExist('lastname').first();
+    expect(john.get('lastname')).toBeUndefined();
+  });
+
+  it('can skip cascade (default true) saving as per request', async () => {
+    const Parent = Parse.Object.extend('Parent');
+    const Child = Parse.Object.extend('Child');
+
+    const parent = new Parent();
+    const child1 = new Child();
+    const child2 = new Child();
+    const child3 = new Child();
+
+    child1.set('name', 'rob');
+    child2.set('name', 'sansa');
+    child3.set('name', 'john');
+    parent.set('children', [child1, child2]);
+    parent.set('bastard', child3);
+
+    // cascadeSave option default true
+    await parent.save(null, {
+      /* cascadeSave: true */
+    });
+    const results = await new Parse.Query(Child).find();
     assert.equal(results.length, 3);
 
     parent.set('dead', true);
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 36453bb..565b57c 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -965,7 +965,7 @@ describe('ParseObject', () => {
       o.validate({
         'invalid!key': 12,
       })
-    ).toEqual(new ParseError(ParseError.INVALID_KEY_NAME));
+    ).toEqual(new ParseError(ParseError.INVALID_KEY_NAME, `Invalid key name: "invalid!key"`));
 
     expect(
       o.validate({
@@ -982,16 +982,13 @@ describe('ParseObject', () => {
 
   it('validates attributes on set()', () => {
     const o = new ParseObject('Listing');
-    expect(o.set('ACL', 'not an acl')).toBe(false);
+    expect(() => {
+      o.set('ACL', 'not an acl');
+    }).toThrow(new ParseError(ParseError.OTHER_CAUSE, 'ACL must be a Parse ACL.'));
     expect(o.set('ACL', { '*': { read: true, write: false } })).toBe(o);
-    expect(o.set('$$$', 'o_O')).toBe(false);
-
-    o.set('$$$', 'o_O', {
-      error: function (obj, err) {
-        expect(obj).toBe(o);
-        expect(err.code).toBe(105);
-      },
-    });
+    expect(() => {
+      o.set('$$$', 'o_O');
+    }).toThrow(new ParseError(ParseError.INVALID_KEY_NAME, `Invalid key name: "$$$"`));
   });
 
   it('ignores validation if ignoreValidation option is passed to set()', () => {
@@ -1947,22 +1944,25 @@ describe('ParseObject', () => {
     await result;
   });
 
-  it('will fail for a circular dependency of non-existing objects', () => {
+  it('will fail for a circular dependency of non-existing objects', async () => {
     const parent = new ParseObject('Item');
     const child = new ParseObject('Item');
     parent.set('child', child);
     child.set('parent', parent);
-    expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.');
+    await expect(parent.save()).rejects.toThrowError(
+      'Cannot create a pointer to an unsaved Object.'
+    );
   });
 
-  it('will fail for deeper unsaved objects', () => {
+  it('will fail for deeper unsaved objects', async () => {
     const parent = new ParseObject('Item');
     const child = new ParseObject('Item');
     const grandchild = new ParseObject('Item');
     parent.set('child', child);
     child.set('child', grandchild);
-
-    expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.');
+    await expect(parent.save()).rejects.toThrowError(
+      'Cannot create a pointer to an unsaved Object.'
+    );
   });
 
   it('does not mark shallow objects as dirty', () => {