JavaScript Basics: Read Object Options With Reliable Defaults
Optional settings look simple until a valid value is 0, false, or an empty string. Another common surprise is copying an object and accidentally changing a nested value in the original. This tutorial uses small local examples to make defaults and object copying predictable.
Prepare a scratch directory
Prerequisites are Linux, Bash, the mktemp, rm, and rmdir utilities, a text editor, and Node.js on your PATH. These examples were tested with Node.js v24.19.0 on Linux. They require no third-party packages, environment variables, network requests, or package manifest. Files ending in .mjs use ES modules, so they can import Node’s built-in assertion module directly.
In Bash, run:
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.0Keep this terminal open. Save the following files as UTF-8 with your editor inside the temporary directory. Each script is independent and includes its own input data.
Destructure options and preserve intentional values
Destructuring extracts object properties into local variables. A destructuring default applies when the property’s value is undefined, including when the property is absent. It does not replace null. See MDN’s destructuring reference.
Save as read-display-options.mjs:
import assert from "node:assert/strict";
function readOptions(input) {
const { title = "Untitled", retries = 3, enabled = true } = input ?? {};
return { title, retries, enabled };
}
const options = readOptions({ title: "", retries: 0, enabled: false });
assert.deepEqual(options, { title: "", retries: 0, enabled: false });
assert.deepEqual(readOptions(), {
title: "Untitled", retries: 3, enabled: true,
});
assert.deepEqual(readOptions(null), readOptions());
assert.equal(readOptions({ title: null }).title, null);
assert.equal(readOptions({ title: undefined }).title, "Untitled");
console.log(JSON.stringify(options));
console.log(`Missing title: ${readOptions().title}`);
console.log(`Null title: ${readOptions({ title: null }).title}`);Run:
node read-display-options.mjsExpected output:
{"title":"","retries":0,"enabled":false}
Missing title: Untitled
Null title: nullThe expression input ?? {} protects destructuring when the entire argument is null or undefined. A default parameter such as input = {} would cover an omitted argument or undefined, but not an explicit null.
There are two separate choices here: which whole inputs count as absent, and which property values deserve defaults. If null should also trigger the title fallback, read the property and apply title ?? "Untitled" instead. Defaults do not validate types; this function expects an options object or a nullish value, not arbitrary untrusted input.
Choose between nullish and truthy defaults
?? falls back only for null or undefined. || falls back for every falsy value, including 0, false, and "". Choose based on what those values mean in your application. MDN explains nullish coalescing.
Optional chaining returns undefined when the value at its guarded step is nullish. Guard every level that might be missing. It works naturally with ?? to provide a final fallback. See MDN’s optional chaining reference.
Save as read-profile-label.mjs:
import assert from "node:assert/strict";
function profileLabel(profile) {
return profile?.preferences?.label ?? "Guest";
}
const profile = { preferences: { label: "", refreshSeconds: 0 } };
const seconds = profile.preferences.refreshSeconds;
assert.equal(seconds ?? 30, 0);
assert.equal(seconds || 30, 30);
assert.equal(false ?? true, false);
assert.equal(false || true, true);
assert.equal(profileLabel(profile), "");
assert.equal(profileLabel({ preferences: null }), "Guest");
assert.equal(profileLabel({}), "Guest");
assert.equal(profileLabel(null), "Guest");
console.log(`Refresh with ??: ${seconds ?? 30}`);
console.log(`Refresh with ||: ${seconds || 30}`);
console.log(`Existing label: ${JSON.stringify(profileLabel(profile))}`);
console.log(`Missing profile: ${profileLabel(undefined)}`);Run:
node read-profile-label.mjsExpected output:
Refresh with ??: 0
Refresh with ||: 30
Existing label: ""
Missing profile: GuestOptional chaining is not general error handling. The root variable must exist, and a present property might still have the wrong type. Keeping the chain continuous also matters: (profile?.preferences).label would try to read label from undefined when the profile is missing.
Copy the level you intend to edit
Object spread creates a shallow copy. Nested objects are still shared references. To update a nested setting without changing the original, copy the outer object and the nested object along the changed path. Later properties in an object literal replace earlier properties with the same key. See MDN’s spread syntax reference.
Save as copy-theme-settings.mjs:
import assert from "node:assert/strict";
const original = { name: "Reader", theme: { mode: "light", size: 16 } };
const shallow = { ...original };
const updated = {
...original,
theme: { ...original.theme, mode: "dark" },
};
assert.notEqual(shallow, original);
assert.equal(shallow.theme, original.theme);
assert.notEqual(updated.theme, original.theme);
updated.theme.size = 18;
assert.equal(original.theme.size, 16);
assert.equal(original.theme.mode, "light");
console.log(`Original: ${JSON.stringify(original)}`);
console.log(`Updated: ${JSON.stringify(updated)}`);Run:
node copy-theme-settings.mjsExpected output:
Original: {"name":"Reader","theme":{"mode":"light","size":16}}
Updated: {"name":"Reader","theme":{"mode":"dark","size":18}}This is a targeted copy, not a universal deep clone. If theme contained another nested object, that deeper object would still be shared unless you copied it too. The example assumes the known plain-object shape shown above.
Clean up
After running all three scripts, remove only these tutorial files:
rm -- read-display-options.mjs read-profile-label.mjs copy-theme-settings.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirBefore choosing a default, ask whether zero, false, an empty string, and null are meaningful. Before changing a copy, ask which nested references it still shares. Those two checks prevent many subtle settings bugs.
Acknowledgements
This post was created with assistance from AI.