Adopt modern JavaScript syntax you will see in every current codebase: modules, destructuring, async/await, and safer defaults.
What you will learn
let/const, arrow functions, template literals- Destructuring, rest, and spread
- Promises and
async/awaitwith real error handling - ES modules in Node and browsers
Prerequisites
- Basic JavaScript: variables, functions, arrays, objects, and the DOM or Node REPL
Why “modern” still matters
Most tutorials still mix var, callbacks, and CommonJS. Production code expects block scoping, modules, and async/await. Learn the current defaults once, then read older snippets with a translation layer in your head.
Start here: MDN JavaScript Guide.
Syntax you should know cold
const user = { id: 1, name: "Ada", roles: ["admin", "editor"] };
const { name, roles: [primaryRole, ...otherRoles] } = user;
const copy = { ...user, name: "Grace" };
const add = (a, b) => a + b;
const message = `Hello, ${name} (${primaryRole})`;
const unique = [...new Set([1, 1, 2, 3])];Prefer const by default; use let only when you reassign. Avoid var.
Async flow
async function loadProject(slug) {
const response = await fetch(`/api/projects/${slug}`);
if (!response.ok) {
throw new Error(`request_failed:${response.status}`);
}
return response.json();
}
async function main() {
try {
const project = await loadProject("postgres");
console.log(project.name);
} catch (error) {
console.error(error);
}
}Docs: async function and Using promises.
Modules
// math.js
export function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
// app.js
import { clamp } from "./math.js";
console.log(clamp(120, 0, 100));In browsers, use <script type="module">. In Node, prefer "type": "module" in package.json or .mjs files. See Node.js ES modules.
Useful extras
- Optional chaining:
user.profile?.city - Nullish coalescing:
value ?? "default"(onlynull/undefined) Array.prototype.map/filter/reduceinstead of manual index loops when clarity wins
Practice checklist
- Rewrite a callback-based
fs.readFileexample withasync/await(orfs/promises) - Split a script into two modules with one named export
- Destructure a nested API response safely with optional chaining
Common pitfalls
- Treating
??like||(empty string and0are valid values) - Forgetting
awaitand logging a Promise object - Mixing CommonJS
requireand ESMimportin the same file without a migration plan
Sources and further reading
- MDN JavaScript Guide
- MDN async function
- ECMAScript spec repo: tc39/ecma262
- Node ESM docs: Node.js ES modules
Who this guide is for
- JavaScript developers modernizing legacy code
- Frontend and Node.js engineers aligning language conventions
- Reviewers assessing readability and runtime compatibility
Definition of done
A small modernized module that uses clear data transformations, explicit async error handling, compatible module boundaries, and tests that preserve behavior through the refactor.
Finishing the commands is not the same as finishing the guide. Keep the result only when another person can reproduce it, inspect the evidence, and understand the remaining risks.
Professional workflow
Phase 1 — Establish compatibility
Goal: Know which syntax and APIs the supported runtimes understand.
- List browser or Node.js support targets.
- Separate language syntax from newer web or runtime APIs.
- Run existing tests before changing style.
Verification: The support matrix and transpilation policy are documented next to the build configuration.
Phase 2 — Modernize for clarity
Goal: Use modern features only where intent becomes easier to read.
- Prefer const and narrow scopes.
- Use destructuring and iteration without hiding meaningful names.
- Replace promise chains with async functions when error flow becomes clearer.
Verification: The refactor changes no externally observable behavior and reduces cognitive branching.
Phase 3 — Strengthen module boundaries
Goal: Make imports, exports, and errors predictable.
- Expose a small public API.
- Avoid hidden global mutation.
- Test rejected promises, empty data, and malformed input.
Verification: Consumers can use the module without relying on internal object shape or execution order.
Review questions before you continue
- Which runtimes must execute this code directly?
- Does the proposed syntax improve intent or only shorten lines?
- Where should asynchronous errors be handled?
- Can mutation be localized rather than eliminated through expensive copying?
If an answer is unknown, record it as an explicit follow-up instead of hiding the uncertainty behind a successful demo.
Troubleshooting decision guide
- An import fails: confirm ESM/CommonJS mode, file extensions, package exports, and runtime support.
- Async errors disappear: await the promise or return it to the caller and test rejection paths.
- Destructuring throws: validate nullable external input before unpacking it.
- A refactor becomes slower: measure the hot path instead of assuming a functional style is free.
Change one variable at a time, preserve the first useful error, and write down the command or condition that fixed it. That turns a private debugging session into team knowledge.
Production-readiness checklist
- Pin and document runtime support.
- Lint for unsafe patterns without turning style into noise.
- Validate all external input.
- Keep source maps available for controlled debugging.
- Test both success and failure behavior at public module boundaries.
These checks are intentionally stricter than a beginner demo. Use them to decide whether to continue learning, run a controlled pilot, or request specialist review.
Your next 30 minutes
- Choose one legacy module with strong existing tests.
- Modernize it without changing the public API.
- Review bundle/runtime compatibility and error paths.
- Adopt only the conventions that improved the review.
CTA: Pick the first unchecked step, complete it now, and save the evidence in your project README or decision log. Then open the official documentation linked below before adopting the result in production.