
Function Declarations vs. Function Expressions vs. Arrow Functions

JavaScript gives developers several ways to define a function, and that flexibility is both useful and slightly awkward for beginners.
You can write a function declaration:
function greet(): void { console.log('Hello');}or a function expression:
const greet = function (): void { console.log('Hello');};or an arrow function:
const greet = (): void => { console.log('Hello');};All three define callable behaviour, but they are not identical. The differences show up in hoisting, naming, this, and in how naturally they fit certain kinds of code.
Function Declarations Stand on Their Own
A function declaration creates a named function directly in the current scope.
function formatPrice(value: number): string { return `£${value.toFixed(2)}`;}This style is often easy to scan because the function's identity is explicit and visible immediately.
It also behaves differently with hoisting. Declarations are hoisted in a way that lets you call them before their position in the file.
console.log(formatPrice(10));function formatPrice(value: number): string { return `£${value.toFixed(2)}`;}That works.
Function Expressions are Values Assigned to Variables
A function expression creates a function as part of an expression, often by assigning it to a variable.
const formatPrice = function (value: number): string { return `£${value.toFixed(2)}`;};The variable itself is hoisted, but not in the same fully callable way as a declaration. If you try to call it before the assignment runs, you will run into trouble.
That is one of the first meaningful differences between declarations and expressions: they may look similar in final form, but their initial timing behaviour differs.
Arrow Functions are a Special Kind of Function Expression
Arrow functions are not a completely separate universe. They are really a more modern shorthand for a function expression, with some important behavioural differences.
const formatPrice = (value: number): string => { return `£${value.toFixed(2)}`;};They are especially common in modern front‑end code because they:
- are concise
- work neatly with callbacks
- fit well with array methods and component logic
But their biggest difference is not syntax. It is how they handle this.
Hoisting is One of the Practical Differences
Function declarations can be called before they appear in the file because of how hoisting works:
announce();function announce(): void { console.log('Ready');}A function expression assigned to a const or let cannot be used before the assignment line has executed:
announce();const announce = (): void => { console.log('Ready');};That fails.
This does not automatically make declarations better. It just means the styles make different promises about when they become available.
Arrow functions do not have their own this
This is probably the single most important behavioural difference.
Regular functions get their own this based on how they are called.
Arrow functions do not create a new this; they close over the surrounding one.
That makes arrow functions very useful for callbacks inside methods:
const timer = { seconds: 0, start(): void { setTimeout(() => { this.seconds += 1; }, 1000); },};Because the arrow function inherits this from start, it keeps the surrounding method context rather than creating a new one.
That is helpful in modern front‑end code, but it also means arrow functions are not the right fit for every job. If you need a function with its own dynamic this, a regular function is more appropriate.
Naming Can Be Clearer with Declarations
One quiet advantage of function declarations is that they present a stable name naturally:
function validateCheckoutForm(): boolean { return true;}That can read very well in larger files.
Function expressions can be just as readable when assigned to well‑named constants:
const validateCheckoutForm = (): boolean => { return true;};So the readability question is often less about which form is objectively superior and more about how the surrounding code is structured.
Arrow Functions Shine in Callbacks
Callbacks are where arrow functions often feel most natural.
const prices = [10, 20, 30];const doubled = prices.map((price) => { return price * 2;});That is a very natural fit because the function is small, local, and expression‑oriented.
You could write the same thing with a function expression:
const doubled = prices.map(function (price) { return price * 2;});but the arrow version tends to feel cleaner in contemporary code.
Declarations are Still Good for Top‑Level Named Helpers
This is the part sometimes lost in "modern JavaScript" conversations.
Function declarations are not outdated. They are still a perfectly sensible choice for named top‑level helpers, especially when you want the function to read like a stable unit in the file.
For example:
function isValidEmail(value: string): boolean { return value.includes('@');}That is direct and readable. There is nothing second‑rate about it.
There is No Universal Winner
This is the real lesson.
Use function declarations when:
- you want a clear named helper
- hoisting behaviour is acceptable or helpful
- the function is a top‑level unit in the file
Use function expressions when:
- you want a function treated as a value explicitly
- the code style of the module prefers assigned helpers
Use arrow functions when:
- you are writing short callbacks
- you want lexical
this - the function is naturally expression‑oriented
The mistake is expecting one form to replace the others entirely.
A Practical Style Rule
If you want a simple rule that works well in modern front‑end code, it is often this:
- declarations for named standalone functions
- arrow functions for callbacks and many local helpers
That is not the only workable style, but it is a sensible default because it aligns the syntax with the job the function is doing.
Wrapping up
Function declarations, function expressions, and arrow functions all define callable logic, but they differ in timing, naming style, and how they handle this. Declarations are strong for named top‑level helpers, expressions treat functions clearly as values, and arrow functions fit especially well with callbacks and lexical this. The best choice is usually the one that matches the role the function plays in the code.
Key Takeaways
- Function declarations are hoisted differently and can be called before their position in the file.
- Function expressions are assigned to variables and are not available in the same way before assignment.
- Arrow functions are a form of function expression with lexical
this. - Arrow functions are especially useful in callbacks and local expression‑style logic.
- None of the three forms is always correct; the best choice depends on what the function is doing.
Once you understand the trade‑offs, these three styles stop looking like arbitrary syntax choices and start feeling like tools for different kinds of jobs.
Related Articles

Rest and Spread Operators in JavaScript: A Beginner's Guide. 
JavaScript Hoisting: Variables, Functions, and More. JavaScript Hoisting: Variables, Functions, and More

Optimising gatsby‑image Even Further. Optimising
gatsby‑imageEven Further
Template Literals in JavaScript: Writing Multi‑Line Strings. Template Literals in JavaScript: Writing Multi‑Line Strings

Building Multi‑Tenant Applications with Next.js. Building Multi‑Tenant Applications with Next.js

CSS aspect‑ratio for Responsive Layouts. CSS
aspect‑ratiofor Responsive LayoutsCreate Arrays of Any Size with Placeholder Content in JavaScript. Create Arrays of Any Size with Placeholder Content in JavaScript
Do Websites Need to Look the Same in Every Browser? Do Websites Need to Look the Same in Every Browser?

Mutation vs. Immutability in JavaScript Arrays and Objects. Mutation vs. Immutability in JavaScript Arrays and Objects

Creating Custom Vue Directives for Enhanced Functionality. Creating Custom Vue Directives for Enhanced Functionality

Understanding CSS Transitions. Understanding CSS Transitions

Object.freeze(), Object.seal(), and preventExtensions(). Object.freeze(),Object.seal(), andpreventExtensions()