The Case Against Arrow Functions
You reached for the arrow because it looked shorter. It cost you the name in your stack trace, hoisting, and any way to tell a function from a value.
Arrow functions turn up everywhere.
And you need them in almost none of those places.
One exception first, because it explains why they exist. An arrow binds no this of its own. That counts as the feature. Inside a class, inside an object method, when a callback needs to see the surrounding this, reach for an arrow. Nothing below applies to you.
Everywhere else, you reached for it because it looked shorter.
The Missing Name
This one costs real time.
const handleSubmit = async (event) => {
await save(event.target);
};
An anonymous function, assigned to a variable. Engines infer a name from the assignment these days, so that line mostly survives. Pass it inline, which happens far more often, and you get nothing:
form.addEventListener("submit", async (event) => { await save(event.target); });
Now it throws in production and your stack shows 4 frames of <anonymous>.
form.addEventListener("submit", async function handleSubmit(event) {
await save(event.target);
});
One word. 8 characters. Now the trace tells you where you stand. Named function expressions remain the most under-used thing in the language.
Losing Hoisting
A function declaration hoists. You can call it above where you wrote it, which lets you put the important thing first and the helpers underneath:
/**
* Turns a set of rows into the report a reader sees.
*
* @param {Row[]} rows what came back from the query
* @returns {string} the formatted report
*/
export function renderReport(rows) {
return format(summarise(rows));
}
function summarise(rows) { /* … */ }
function format(summary) { /* … */ }
Rewrite those 2 helpers as const summarise = (rows) => … and now they have to come before the thing using them. Your file falls into dependency order instead of reading order. Most important thing at the bottom. Nobody reads anything that way.
Function Or Value
const total = calculateTotal(items);
const format = (value) => value.toFixed(2);
2 consts. One holds a number, one holds a function, and you read to the end of the line to find out which. Do that 30 times in a file and everything starts to look like data.
function gives you a keyword. It shows up at the start of the line and tells you what the line does.
About useState
The class-to-function migration in React got sold as a move toward functional programming. It moved nowhere near.
const [count, setCount] = useState(0);
Functional programming asks 2 things of a function. Stay pure, meaning no side effects. Stay deterministic, meaning same input, same output. useState manages neither. It mutates something outside the call, and it hands back a different value on every render for the same argument.
None of which counts against hooks. They give you a good API and I use them. It counts against the word, because calling a component a “function component” convinced a lot of people that writing everything as an arrow somehow made their code more functional. It did nothing of the sort. Same object-oriented state, object hidden.
What I Do
The rule I follow, and the one this site’s linter enforces:
1. function declarations for anything named. Module-level helpers, exported utilities, handlers you assign once.
2. Named function expressions for callbacks. Reads no worse. Traces read far better.
/** The tag every post filed under a category shares. */
const CATEGORY_TAG = /^category-/;
const categories = tags.filter(function isCategory(tag) {
return CATEGORY_TAG.test(tag);
});
3. Arrows for their actual job. A short callback needing the enclosing this, or one so trivial a name would add noise. A comparator. A .filter(Boolean).
And a 4th, which really belongs to something else.
Stop putting functions everywhere.
A utils folder smells. Things land there when nobody decided what to call them. Half of yours wants to become a service, something with a job and a boundary, and the other half wants to become a method on a type nobody wrote. I argue for neither classes everywhere nor functions everywhere. I argue that the thing deserves a name and a home:
/**
* Everything the site knows about how long a post takes to read.
*
* @param {string} markdown the post's body
* @returns {number} whole minutes, never fewer than 1
*/
export const readingTimeInMinutes = function (markdown) {
return Math.max(1, Math.ceil(countWords(markdown) / WORDS_PER_MINUTE));
};
Your debugger will tell you when you got this wrong. 30 frames deep, half of them anonymous? The arrows caused none of that. They only made it harder to see.