Skip to content
Chapter 2Lesson 1

Arrow functions vs. function declarations

The JavaScript and TypeScript convention for choosing between arrow functions and function declarations.

Open a file from a team that never picked a function rule and you’ll see all three forms in the same hundred lines: a function declareUser() { ... } at the top, a const handleClick = function (e) { ... } halfway down, and a (name) => name.toUpperCase() inside a .map. Every one is legal, but none was chosen for a reason. The problem isn’t any single form, it’s the rotation: a reviewer can’t tell whether a function keyword carried meaning or was just the author’s next reflex.

The fix is to pick one default and name the few cases that flip it. The course’s default is const fn = (args) => …, and three narrow triggers earn a function declaration. Everything else stays const plus arrow.

This extends the const-by-default reflex from the previous lesson, “const, let, and scope,” to functions. If every binding is const, then every function is a value bound to const, which is exactly const fn = (args) => ….

Here is the canonical shape, and every function in the rest of the course follows it unless one of three triggers flips it.

const greet = (name: string) => `Hello, ${name}`;

This binds an arrow expression to const: greet is the binding, the right-hand side is the function value, and the arrow joins the parameter list to the body.

Three reasons this wins as the default:

  • It continues the const reflex from the previous chapter. Functions are values, so if every binding is const unless it has to reassign, the function form follows: const fn = …. You are not learning a new rule, just applying the one you already have.
  • No this rebinding surprise inside callbacks. Arrow functions inherit this from the enclosing scope, while function expressions get their own this from the call site. Inherited this is what .map, event handlers, and setTimeout want by default.
  • Biome’s useArrowFunction rule backs it up at build time. It auto-fixes non-this, non-generator function expressions into arrow form and leaves top-level function declarations alone. Like the useConst rule before it, you write what you mean and the linter catches the cases where the reflex slipped.

Anything outside the three triggers stays const plus arrow.

Implicit return, block body, and the parens trap

Section titled “Implicit return, block body, and the parens trap”

Arrow functions have two body shapes, and a single condition picks between them.

const double = (x: number) => x * 2;

One expression, no braces, no return. The expression’s value is what the function returns. Use this form when the body is one line.

Reach for the block body as soon as a statement appears. Don’t twist a return into a comma expression or a nested ternary to keep the one-line shape; that only makes the next reader work harder.

Arrow functions have one syntax gotcha, and it catches nearly every beginner once.

const wrong = (name: string) => { name: name.trim() };
const right = (name: string) => ({ name: name.trim() });

The braces parse as a block body, not an object literal. The parser reads name: as a labeled statement and name.trim() as the expression inside it. There is no return, so the function returns undefined. Biome and a strict tsconfig usually flag the unused label, but a looser config stays silent: no error, just an undefined flowing downstream until something breaks.

const wrong = (name: string) => { name: name.trim() };
const right = (name: string) => ({ name: name.trim() });

Parens around the object literal remove the ambiguity: the braces are now an object, and the parens are the expression the arrow returns. Wrap object-literal returns in parens by habit and the trap disappears.

1 / 1

The three triggers that earn a function declaration

Section titled “The three triggers that earn a function declaration”

These are the only cases where function earns its keyword in modern code.

Hoisting: top-of-file helpers used above their declaration

Section titled “Hoisting: top-of-file helpers used above their declaration”

A function declaration hoists its full body to the top of the scope, so it’s callable from any line in the module, including lines above it. An arrow const lives in the Temporal Dead Zone (from “const, let, and scope”) until its own line, so it can’t be referenced before that.

const render = (node: TreeNode) => `<li>${formatLabel(node)}</li>`;
function formatLabel(node: TreeNode) {
return node.label.trim();
}

formatLabel is callable on line 1 because function hoists its whole body. Rewrite it as an arrow const and line 1 throws ReferenceError: Cannot access 'formatLabel' before initialization.

This trigger is rare: modern code is import-first, so most helpers come from other files, not from lower in the same file.

Named recursion: a helper that refers to itself

Section titled “Named recursion: a helper that refers to itself”

A recursive function needs a stable name to call itself by, and the function form gives that name directly to the function’s own scope, independent of any outer binding.

function walk(node: TreeNode): void {
for (const child of node.children) {
walk(child);
}
}

The arrow alternative, const walk = (node) => { ... walk(child); ... }, works, but its recursion routes through the outer const binding. The moment another scope shadows walk (a test that mocks it, an inner helper that reuses the name), the recursion calls the shadow instead of itself. The function form sidesteps the question by recursing through its own name.

This trigger covers tree walkers, parsers, and a few recursive algorithm helpers.

Type-guard and assertion signatures: TypeScript-shaped triggers

Section titled “Type-guard and assertion signatures: TypeScript-shaped triggers”

This is the trigger most students miss on first read. TypeScript has two signature shapes that interact with arrow vs. function:

  • The assertion signature asserts x is User does not parse on an arrow at all: const assertUser = (x: unknown): asserts x is User => … is a syntax error. Assertions must be function.
  • The predicate signature x is User has more nuance. TypeScript 5.5+ can infer a predicate from a simple-bodied arrow (e.g. const isUser = (x: unknown) => typeof x === 'object' && x !== null && 'id' in x). But when the body grows past one expression, or you want the contract declared rather than inferred, function is the canonical form: the signature states the contract, so no inference is required.

Declare both shapes with function so the predicate sits next to its assertion sibling and reviewers read the contract at a glance. Here’s the pair in one block; narrowing fires once the predicate returns true.

The assertion asserts x is User only parses on function. The predicate x is User can be inferred on simple arrows since TS 5.5, but the canonical declared form for both is function — the signature reads the contract, no inference required. Watch the ^? query inside the if branch: value started as unknown; after the predicate fires it narrows to User.

  • Type query at line 14 must resolve to a type containing User
Booting type-checker…

Predicates, assertions, and the full narrowing model get their own treatment in the later typing chapters; here you only need to recognize the shape that earns a function.

Two more function forms exist; neither is a trigger, so recognize them without converting them to arrows.

A method on an object literal can drop the colon and the function keyword:

const config = {
greet(name: string) {
return `Hello, ${name}`;
},
};

It binds its own this from the call site, the correct behavior for an object method, so leave it as written.

A function expression can be assigned to a const:

const factorial = function fact(n: number): number {
return n <= 1 ? 1 : n * fact(n - 1);
};

The internal name (fact) is visible only inside the body, allowing self-reference. Modern code uses an arrow or a function declaration instead; recognize this form in older code and libraries, but don’t write it.

How this differs in arrows and function forms

Section titled “How this differs in arrows and function forms”

Arrow functions inherit this from the enclosing scope. function forms (declarations, expressions, method shorthand) get their own this based on the call site.

Before arrow functions arrived in 2015, a function callback got its own this, usually undefined or the wrong object, so developers wrote .bind(this) on every callback to fix it. Arrows inherit this from where they’re written, which is what callbacks want, so the old pain is gone the moment you reach for an arrow const.

The functional stack you’ll learn barely uses this: components, database queries, and Server Actions are all plain functions. The only this you’ll meet is in third-party class-based code, such as some SDKs. If you find yourself reaching for .bind(this) or wondering what this resolves to in your own code, you’ve drifted into class-OOP territory the course doesn’t teach; step back, write a function that takes what it needs as parameters, and the question disappears.

For the strongest version of the opposing case, hear out a developer who weighs the same hoisting and this mechanics and lands on function by default.

Sort these eight situations into two buckets yourself — that is what makes the choice automatic.

Sort each function into the form a 2026 senior would reach for. Three of these earn a `function` declaration — the other five are arrow `const`. Drag each item into the bucket it belongs to, then press Check.

arrow const `const fn = (args) => …` — the 2026 default.
function declaration `function name(args) { ... }` — earned by one of the three triggers.
An onClick callback inside a button
A .map projection that builds a row
A recursive tree walker that calls itself by name
A function isInvoice(x: unknown): x is Invoice predicate
A React component
A top-of-file format(node) helper called by render(node) declared above it
A Server Action exported by name
A short utility const isAdult = (age) => age >= 18

The reflex to leave with: default to an arrow const, and reach for function only when one of the three triggers applies.