Wednesday 14 August 2024

JavaScript Destructuring Assignment Gotcha

This wasted several hours on me. I'll explain exactly what I was doing that got me into this mess a little later, but the bare essence of the problem that I ran into was this:-

let a = "this is a" let b = "this is b" [a,b] = ["foo", "bar"] console.log(a) console.log(b)

What would you expect it to output? I expected:-

foo bar

That's not what I got, though:-

$ node foo.js /home/eamonn/tmp/foo.js:4 [a,b] = ["foo", "bar"] ^ ReferenceError: Cannot access 'b' before initialization at Object. (/home/eamonn/tmp/foo.js:4:4) at Module._compile (node:internal/modules/cjs/loader:1358:14) at Module._extensions..js (node:internal/modules/cjs/loader:1416:10) at Module.load (node:internal/modules/cjs/loader:1208:32) at Module._load (node:internal/modules/cjs/loader:1024:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12) at node:internal/main/run_main_module:28:49

The error-message made no sense to me. To fast-forward past lots of tearing-out-of-hair, changing the code to this (note the addition of the ";" on the second line):-

let a = "this is a" let b = "this is b"; [a,b] = ["foo", "bar"] console.log(a) console.log(b)

...makes everything work as it should:-

$ node foo.js foo bar

So what's going on? The answer is that the two lines...

let b = "this is b" [a,b] = ["foo", "bar"]

...are actually parsed as:-

let b = "this is b"[a,b] = ["foo", "bar"]

Note that b is being used to to try to dereference the "array" that is the string "this is b". But, of course, b isn't defined at this point so can't be used to dereference anything (and in this light the error-message makes more sense).

My JavaScript coding style is was to omit trailing ";" on the basis that they aren't necessary. It turns out that this is only true most of the time. I'm starting to seriously rethink that style now.

What was I doing? I had two "fetch" commands that I wanted to kick off in parallel. I had some processing to do that depended on the results of both of them. Rather than running them sequentially, I wanted to do this:-

(async () => { let first_response let second_response try { // Schedule the two fetches to run asynchronously const first_promise = fetch("http://my.server.ie/firstthing.json") const second_promise = fetch("http://my.server.ie/secondthing.json") // Wait for the two fetches to complete [first_response, second_response] = await Promise.all([first_promise, second_promise]) } catch(error) { ...do something if either of the fetch() calls craps out } // Process the retrieved data const first_result = await first_response.json() const second_result = await second_response.json() process_results(first_result, second_result) })()

But it would fail:-

$ node baa.js ReferenceError: Cannot access 'second_promise' before initialization at /home/eamonn/tmp/baa.js:12:79 [...]

Adding in that semicolon to clear up the "ambiguity" about the destructuring assignment solves the problem just fine:-

(async () => { let first_response let second_response try { // Schedule the two fetches to run asynchronously const first_promise = fetch("http://my.server.ie/firstthing.json") const second_promise = fetch("http://my.server.ie/secondthing.json"); // Wait for the two fetches to complete [first_response, second_response] = await Promise.all([first_promise, second_promise]) } catch(error) { ...do something if either of the fetch() calls craps out } // Process the retrieved data const first_result = await first_response.json() const second_result = await second_response.json() process_results(first_result, second_result) })()

...and the code works as it should

References

I got the clue I needed from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment. There is a particular sentence buried in there:-

If your coding style does not include trailing semicolons, the ( ... ) expression needs to be preceded by a semicolon, or it may be used to execute a function on the previous line.

Not exactly my issue, but close enough that it pointed me in the right direction. Thanks, Mozilla Developer Network, you are endlessly useful and informative.

Having seen the light, I did some research into the merits and demerits of relying on automatic semicolon insertion. The (now deprecated in favour of Typescript) Google JavaScript Style Guide is pretty emphatic (https://google.github.io/styleguide/jsguide.html#formatting-semicolons-are-required):-

Every statement must be terminated with a semicolon. Relying on automatic semicolon insertion is forbidden.

That's good enough for me.