Lập trình · 21/10/2026

JavaScript Object.assign: A Failed Call Can Still Partially Modify Its Target

Object.assign throws TypeError, yet some target properties have already changed. It does not roll back successful writes when a later step fails. State and configuration updates need explicit decisions about source reads, target writes and publication.

Object.assign trong JavaScript: lệnh lỗi nhưng object vẫn bị sửa một phần

Object.assign throws TypeError, yet some target properties have already changed. It does not roll back successful writes when a later step fails. State and configuration updates need explicit decisions about source reads, target writes and publication.

1. Failure does not imply an unchanged target

const target = { first: 0 };
Object.defineProperty(target, 'locked', { value: 0, writable: false });
try {
  Object.assign(target, { first: 1, locked: 2, last: 3 });
} catch (error) {
  console.log(error instanceof TypeError); // true
}
console.log(target.first); // 1
console.log(target.locked); // 0
console.log(Object.hasOwn(target, 'last')); // false

first changes before the write to locked fails, while last is never reached. A catch that merely logs and continues can leave partially updated application state.

2. Target writes can invoke setters

const writes = [];
const target = {
  set mode(value) { writes.push(value); }
};
Object.assign(target, { mode: 'dark' });
console.log(writes.join(',')); // dark
const copy = { ...target, mode: 'light' };
console.log(writes.join(',')); // dark
console.log(copy.mode); // light

assign writes through the target's setter. Object spread creates data properties on a new object without using that setter to write light. Accessors make the operations observably different.

3. Source reads can invoke getters

let reads = 0;
const source = {
  get count() { reads += 1; return 7; }
};
const assigned = Object.assign({}, source);
const spread = { ...source };
console.log(reads); // 2
console.log(assigned.count); // 7
console.log(typeof Object.getOwnPropertyDescriptor(assigned, 'count').get); // undefined
console.log(spread.count); // 7

Both operations execute the getter here. The assigned result holds the retrieved value rather than the source getter. Descriptor preservation or instance behavior requires a different copying contract. Getters can throw or cause side effects, so switching to spread does not make copying infallible or pure.

4. Construct a candidate before replacing state

let state = { mode: 'light' };
const previous = state;
let reads = 0;
const patch = {
  get mode() { reads += 1; throw new Error('read failed'); }
};
try {
  state = { ...state, ...patch };
} catch (error) {
  console.log(error.message); // read failed
}
console.log(state === previous); // true
console.log(state.mode); // light
console.log(reads); // 1

The right-hand expression fails before assignment, preserving the old state reference in this example. Construct a candidate, validate its schema and publish it through the framework's intended update mechanism.

This is not a general rollback transaction: the getter has already run. Nested objects remain shallowly shared, and external side effects are not undone. Target ordinary application data deliberately rather than applying this mechanically to class instances or behavior-rich objects.

5. Inspect state after failures

  • Identify successful writes before an assign failure.
  • Check getter and setter invocation.
  • Distinguish values from descriptors.
  • Verify old-state identity when candidate construction fails.
  • Validate before publishing a new state.

All eight bilingual code blocks ran with Node.js, alongside partial-write and descriptor checks. Framework integration and performance were not tested. References: MDN Object.assign and MDN Spread syntax.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.