Arrow by default, declaration on demand
A JavaScript and TypeScript code-style 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 author just reached for whichever form came first. The problem isn’t any single form, it’s the rotation, because a reviewer can’t tell whether a function keyword carried meaning or was simply 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) => …. Three narrow triggers earn a function declaration, and everything else stays const plus arrow.
This lesson extends the const-by-default reflex from the previous chapter’s lesson “const binds, it doesn’t freeze” to functions. If every binding is const, then every function is a value bound to const, which is exactly const fn = (args) => …. You’ll see why arrow const wins as a default, the one syntax gotcha that catches everyone in their first week, the three triggers that earn function, two more forms to recognize but not reach for, and a sorting drill that makes the choice automatic.
Why arrow const is the 2026 default
Section titled “Why arrow const is the 2026 default”Here is the canonical shape. Every function in the rest of the course will look like this, unless one of three triggers flips it.
const greet = (name: string) => `Hello, ${name}`;This is an arrow expression bound to const. Read it left to right: the name (greet) is a const binding, the right-hand side is a function value, and the arrow connects the parameter list to the body. The form follows directly from the previous chapter, because the only function syntax that fits on the right-hand side of a const binding is an arrow expression.
Three reasons this wins as the default:
- It continues the
constreflex from the previous chapter. Functions are values. If every binding isconstunless it has to reassign, then the function form follows:const fn = …. You’re not learning a new rule, just applying the one you already have. - No
thisrebinding surprise inside callbacks. Arrow functions inheritthisfrom the enclosing scope.functionexpressions get their ownthisbased on the call site, which used to force developers to write.bind(this)on every callback before 2015. The arrow’s inherited-thisrule is exactly what.map, event handlers, andsetTimeoutwant by default. The full rule gets a short section of its own later. - Biome’s
useArrowFunctionrule is the build-time backstop. The rule auto-fixes non-this, non-generatorfunctionexpressions into arrow form, and deliberately leaves top-levelfunctiondeclarations alone (those are the triggers covered in the next section). This is the same shape as theuseConstrule from the previous chapter: you write what you mean, and the linter catches the cases where the reflex slipped.
A default is not a rule that always holds. It exists so you can spend your attention on the exceptions instead of re-deciding the common case every time. This lesson teaches both the default and the three named cases where it flips; anything outside those three 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. The choice between them is mechanical: a single condition flips it, and once you’ve seen the rule you won’t have to think about it again.
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.
const double = (x: number) => { const result = x * 2; return result;};Braces and return the moment the body needs a statement: an intermediate const, an if, a loop, a side effect. The block form scales up; the implicit-return form does not.
The senior reflex is to use the implicit-return form for one-liners and switch to the block body as soon as a statement appears. Don’t twist a return into a comma expression or a nested ternary just to keep the one-line shape; that only makes the next reader work harder.
Arrow functions have one syntax gotcha, and it catches every beginner in their first week. The good news is that once you’ve seen it, you’ll never write it again.
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 (an obscure JavaScript feature) and name.trim() as the expression inside it. There is no return, so the function returns undefined. With Biome and a strict tsconfig the unused label is usually flagged at build time, but in older or looser configurations the bug is silent: no syntax error, no type 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. Make wrapping object-literal returns in parens a habit, and the trap disappears.
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 a 2026 SaaS file. Each subsection covers one pattern: the trigger, a snippet, and an honest note on how often it actually shows up.
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. The function is callable from any line in the module, including lines above it. An arrow const lives in the Temporal Dead Zone (introduced in the previous chapter’s lesson “const binds, it doesn’t freeze”) until the line it’s declared on, so it cannot be referenced before that line.
const render = (node: TreeNode) => `<li>${formatLabel(node)}</li>`;
function formatLabel(node: TreeNode) { return node.label.trim();}formatLabel is callable on line 1 because the function form hoists its whole body. If you rewrote formatLabel as an arrow const, the reference on line 1 would throw ReferenceError: Cannot access 'formatLabel' before initialization.
In practice, this trigger is rare in 2026. Most code is import-first and module-scoped, so helpers come from other files, not from lower down in the same file. You’ll see it most often in recursive helpers (the next subsection) and in a handful of small in-module utilities where flipping the declaration order would hurt readability.
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. 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); }}walk refers to itself by name. That name lives in the function’s own scope, not in the outer const binding. The arrow alternative, const walk = (node) => { ... walk(child); ... }, works in practice, but the recursion routes through the outer binding instead of through the function itself. The moment another scope shadows walk (a test that mocks it, an inner helper that reuses the name), the recursion calls the shadow, not itself. The function form sidesteps the whole question.
This trigger covers tree walkers, parsers, and a small set of recursive algorithm helpers. You’ll meet the shape later when traversing database relation graphs and transforming validated input.
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 Userdoes not parse on arrow expressions at all. This is a long-standing language constraint:const assertUser = (x: unknown): asserts x is User => …is a syntax error. You must writefunctionfor assertions. - The predicate signature
x is Userhas 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 is more than one expression, or the team wants the contract declared rather than inferred,functionis the canonical form: the signature states the contract, so no inference is required.
The senior habit in 2026 is to declare both shapes with function. The predicate sits next to its assertion sibling, and reviewers read the contract at a glance. Here’s the pair in a single 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
To be clear about scope: the predicate and the assertion are deep TypeScript topics, and the narrowing and exhaustiveness mechanics get full treatment in the upcoming chapters on typing values and modeling state. For now you only need to recognize the shape that earns a function declaration.
Two forms to recognize, not reach for
Section titled “Two forms to recognize, not reach for”Two more function forms exist in 2026 TypeScript. Neither is a trigger; you only need to recognize them. You’ll see them in third-party code and the occasional config object, and knowing the shape means you won’t “fix” them to an arrow.
Method shorthand in object literals
Section titled “Method shorthand in object literals”When a function lives inside an object literal as a method, the language lets you drop the colon and the function keyword:
const config = { greet(name: string) { return `Hello, ${name}`; },};This form shows up inside config objects, database relation definitions, validation schema transforms, and route-handler exports: anywhere a function lives as a method on an object literal. It’s not a separate trigger, just a syntactic shortcut. Method shorthand binds this the same way function does (its own this based on the call site), which is the correct behavior for an object method. When you meet the form, leave it as written.
function expressions
Section titled “function expressions”The other ancillary form is a function expression assigned to a const.
const factorial = function fact(n: number): number { return n <= 1 ? 1 : n * fact(n - 1);};The internal name (fact) is only visible inside the function body, which is useful for self-reference in an expression that for some reason can’t be a function declaration. In modern code this form is almost always replaced by an arrow expression or a function declaration. It’s named here so you recognize it in older code and third-party libraries; the course doesn’t write it.
The this rule, stated once
Section titled “The this rule, stated once”Arrow functions inherit this from the enclosing scope. function forms (declarations, expressions, method shorthand) get their own this based on the call site. That’s the whole rule.
In any JavaScript article from before 2015, this reads as one of the language’s worst rough edges. The reason is that function expressions used inside callbacks got their own this, usually undefined or the wrong object, so developers had to write .bind(this) on every callback to fix it. Arrow functions arrived in 2015 and removed the problem by inheriting this from where the arrow was written, which is exactly what callbacks want. The pain is real history, but the fix is the ordinary default you’re already using by writing arrow const.
The course barely uses this at all. The stack you’ll learn is functional: components are functions, database queries are functions, Server Actions are functions. The only this you’ll meet is in third-party class-based code (some SDKs) and the rare custom Error subclass. The rule is stated here so you’re not surprised when you see it; it isn’t explored in depth, because you won’t write it.
It’s also worth hearing the strongest version of the opposing case: someone who argues for function by default, working from the same hoisting and this mechanics this lesson uses but weighing them to land on the other choice.
Check yourself
Section titled “Check yourself”Here are eight real situations to sort into two buckets. Three of them earn a function declaration; the other five are arrow const. Sorting them yourself 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.
onClick callback inside a button.map projection that builds a rowfunction isInvoice(x: unknown): x is Invoice predicateformat(node) helper called by render(node) declared above itconst isAdult = (age) => age >= 18The reflex to leave with is simple: default arrow, three triggers earn function. When you write your first React component later in the course, it will be an arrow const. When you write your first Server Action, it will be an async arrow const exported by name. When you write your first type predicate, it will be a function declaration because the signature requires it. Each of those choices was decided here; the form is settled once, and the rest of the course writes it without revisiting the question.
External resources
Section titled “External resources”The canonical reference for arrow-function syntax and the lexical-`this` rule.
The build-time backstop that auto-fixes `function` expressions to arrow form where the conversion is safe — skips top-level declarations, `this`-using functions, and generators.
Official chapter covering type predicates (`x is T`), assertion functions (`asserts x is T`), and the full narrowing model behind the third trigger.
James Sinclair's 2025 decision-flowchart take on the same question — hoisting, `this`, generators — landing in roughly the same place this lesson does.