Cyclomatic Complexity: What It Is, How to Calculate It, and Why It Matters
Cyclomatic complexity measures how complicated your code’s control flow is by counting independent execution paths. Learn how to calculate it, understand its impact on testing and maintainability, and reduce unnecessary complexity with practical JavaScript examples.
After spending a lot of time developing in a certain codebase, you will eventually come across a method that resembles the following:
function processOrder(order) {
if (order) {
if (order.isPaid) {
if (order.isInStock) {
if (order.isVerified) {
return "Process order";
}
}
}
}
return "Cannot process order";
}The function isn't particularly large, but understanding all the possible paths through it already takes some mental effort.
This is where cyclomatic complexity becomes useful.
Cyclomatic complexity is a software metric that measures the number of independent paths through a program's control flow. In simpler terms, it gives you an idea of how complicated the decision-making inside your code is.
The higher the cyclomatic complexity, the more paths you potentially need to think about, test, and maintain.
In this article, we'll look at:
- What cyclomatic complexity actually means
- How to calculate it
- Practical JavaScript examples
- How cyclomatic complexity relates to testing
- What different complexity numbers can tell you
- Why high cyclomatic complexity can become a maintenance problem
- How to reduce cyclomatic complexity
- Whether you should always aim for a low number
What Is Cyclomatic Complexity?
Cyclomatic complexity is a metric introduced by Thomas McCabe in 1976. It measures the number of linearly independent paths through a program's control flow.
Don't let the terminology make it sound more complicated than it is.
Imagine a function like this:
function canDrive(age) {
if (age >= 18) {
return true;
}
return false;
}There are two possible paths:
age >= 18is trueage >= 18is false
So the cyclomatic complexity is 2.
Now add another decision:
function canDrive(age, hasLicense) {
if (age >= 18 && hasLicense) {
return true;
}
return false;
}There is now more decision-making involved.
The important thing to understand is that cyclomatic complexity isn't simply a measure of how many lines a function contains.
A 100-line function can have relatively straightforward control flow, while a 20-line function can have a surprisingly high cyclomatic complexity.
The metric is primarily concerned with decision points and control flow.
Why Does Cyclomatic Complexity Matter?
At first, cyclomatic complexity might seem like another number developers have to worry about.
But it can be useful for several practical reasons.
1. It helps identify complicated code
A function with lots of conditions, loops, and branches can become difficult to understand.
Cyclomatic complexity gives you a measurable indication that the function may deserve a closer look.
2. It helps with testing
Every independent path represents a different route through your code.
More paths generally mean more scenarios that should be considered when writing tests.
3. It can reveal functions doing too much
If a single function has a very high complexity, it may be responsible for several different things.
That can be a sign that the function should be split into smaller pieces.
4. It helps with code reviews
Instead of saying:
"This function feels too complicated."
You can investigate its control flow and see exactly where the complexity comes from.
A Simple Cyclomatic Complexity Example
Consider this function:
function getDiscount(customer) {
if (customer.isPremium) {
return 20;
}
return 5;
}There is one decision:
customer.isPremiumTherefore:
Cyclomatic Complexity = 2
Why 2?
Because there are two possible paths:
isPremium?
/ \
true false
| |
20% 5%The two paths are:
- Premium customer → 20%
- Non-premium customer → 5%
Adding More Decisions
Now let's make the function slightly more complicated:
function getDiscount(customer) {
if (customer.isPremium) {
if (customer.hasCoupon) {
return 30;
}
return 20;
}
return 5;
}Now we have two decision points:
isPremium?
|
├── yes → hasCoupon?
│ ├── yes → 30%
│ └── no → 20%
│
└── no → 5%There are three independent paths.
Therefore:
Cyclomatic Complexity = 3
The important pattern here is:
Each additional decision point generally increases cyclomatic complexity.
The Basic Formula
One common formula for calculating cyclomatic complexity is:
M = E - N + 2PWhere:
M= cyclomatic complexityE= number of edges in the control-flow graphN= number of nodesP= number of connected components
For a single function, P is normally 1.
That gives us:
M = E - N + 2However, you don't usually need to draw a graph and count nodes and edges manually.
For ordinary code, a much simpler approach is often used:
Cyclomatic Complexity = Number of decision points + 1Depending on the language and the tool you're using, the exact treatment of individual constructs can vary.
What Counts as a Decision?
This is where things become more interesting.
Common decision points include:
ifelse ifforwhiledo...whilecasein some counting conventionscatch- Conditional/ternary expressions
- Logical conditions such as
&&and||, depending on the metric/tool
Let's look at some examples.
if Statements
Consider:
function checkAge(age) {
if (age >= 18) {
return "Adult";
}
return "Minor";
}There is one decision.
Complexity = 1 + 1 = 2Multiple if Statements
function checkUser(user) {
if (!user) {
return false;
}
if (!user.email) {
return false;
}
if (!user.active) {
return false;
}
return true;
}There are three decision points.
So:
Complexity = 3 + 1 = 4Notice that the function isn't particularly long.
But there are several possible execution paths.
else if Adds Another Branch
Consider:
function getRole(user) {
if (user.role === "admin") {
return "Administrator";
} else if (user.role === "manager") {
return "Manager";
} else if (user.role === "employee") {
return "Employee";
}
return "Unknown";
}There are three decision points:
if
else if
else ifTherefore:
Cyclomatic Complexity = 4The possible routes are:
- Admin
- Manager
- Employee
- Unknown
What About Loops?
Loops also introduce additional control-flow paths.
For example:
function findUser(users, id) {
for (const user of users) {
if (user.id === id) {
return user;
}
}
return null;
}There are two decisions here:
for loop
if conditionSo a simple decision-counting calculation gives:
Complexity = 3The loop matters because the code can either continue iterating or leave the loop.
Logical Operators Can Increase Complexity
Consider:
function canAccess(user) {
if (user.isAdmin && user.isActive) {
return true;
}
return false;
}This is an interesting case.
Some complexity tools count the && as an additional logical decision, while others may count only the if.
So depending on the tool and metric configuration, you may see different numbers.
Conceptually, however, the important point is that this condition contains multiple logical requirements:
isAdmin AND isActiveAnd therefore there are more combinations to consider when testing the function.
For example:
isAdmin isActive
------------------
true true
true false
false true
false falseThis is one reason why you should treat cyclomatic complexity as a metric, rather than an absolute definition of how difficult a function is.
Cyclomatic Complexity and Unit Testing
One of the most practical uses of cyclomatic complexity is understanding testing requirements.
Consider:
function calculateShipping(order) {
if (order.isInternational) {
return 30;
}
if (order.total >= 100) {
return 0;
}
return 10;
}There are two decision points:
isInternational?
total >= 100?So:
Cyclomatic Complexity = 3There are three basic independent paths:
Path 1
isInternational = trueResult:
$30Path 2
isInternational = false
total >= 100 = trueResult:
$0Path 3
isInternational = false
total >= 100 = falseResult:
$10At minimum, these independent paths should be represented in your tests if you're aiming for basic path coverage.
For example:
test("charges international shipping", () => {
expect(
calculateShipping({
isInternational: true,
total: 50
})
).toBe(30);
});
test("provides free domestic shipping over $100", () => {
expect(
calculateShipping({
isInternational: false,
total: 100
})
).toBe(0);
});
test("charges normal domestic shipping", () => {
expect(
calculateShipping({
isInternational: false,
total: 50
})
).toBe(10);
});This is where cyclomatic complexity becomes more than just a theoretical number.
It can help you reason about how many independent scenarios your tests need to cover.
High Cyclomatic Complexity
Now consider a function like this:
function processPayment(user, order) {
if (!user) {
return "Invalid user";
}
if (!user.active) {
return "Inactive user";
}
if (!order) {
return "Invalid order";
}
if (order.cancelled) {
return "Cancelled order";
}
if (order.total <= 0) {
return "Invalid amount";
}
if (user.balance < order.total) {
return "Insufficient balance";
}
if (order.currency !== user.currency) {
return "Currency mismatch";
}
if (order.requiresVerification && !user.verified) {
return "Verification required";
}
return "Payment processed";
}This function isn't enormous.
But it contains many independent decisions.
As more conditions are added, the number of possible execution paths grows.
That's the real problem.
The code becomes harder to:
- Understand
- Test
- Modify
- Review
- Debug
- Reason about safely
And adding another condition becomes increasingly expensive.
Cyclomatic Complexity vs Lines of Code
It's important not to confuse cyclomatic complexity with code size.
For example:
function greet(name) {
console.log("Hello");
console.log(name);
console.log("Welcome");
console.log("Have a great day");
}This function has several lines, but virtually no decision-making.
Its cyclomatic complexity is low.
Now compare it with:
function greet(user) {
if (!user) return;
if (user.active) {
if (user.isPremium) {
if (user.hasDiscount) {
console.log("Premium discount");
}
}
}
}This function is much shorter but has substantially more control-flow complexity.
So:
Lines of code measure size. Cyclomatic complexity measures control-flow complexity.
Neither metric tells the whole story.
What Is a Good Cyclomatic Complexity?
There isn't a universal number that means:
"This code is good."
Different teams and tools use different thresholds.
A commonly used rule of thumb is:
| Complexity | General interpretation |
|---|---|
| 1–5 | Low complexity |
| 6–10 | Moderate complexity |
| 11–20 | High complexity |
| 20+ | Very high complexity |
These aren't laws.
They're useful warning signals.
A complexity of 8 doesn't automatically mean your code is bad, and a complexity of 3 doesn't automatically mean your code is well-designed.
For example, a parser or state machine may naturally require more branches than a simple utility function.
Context matters.
When Should You Refactor?
Suppose you encounter this:
function calculatePrice(product, user) {
if (user.isPremium) {
if (product.category === "electronics") {
if (product.price > 1000) {
return product.price * 0.8;
}
return product.price * 0.9;
}
if (product.category === "clothing") {
return product.price * 0.85;
}
return product.price * 0.95;
}
if (product.price > 1000) {
return product.price * 0.95;
}
return product.price;
}This isn't necessarily terrible code.
But the nested conditions make the logic increasingly difficult to follow.
We could separate some of the responsibilities.
For example:
function getDiscount(product, user) {
if (!user.isPremium) {
return product.price > 1000 ? 0.05 : 0;
}
if (product.category === "electronics") {
return product.price > 1000 ? 0.20 : 0.10;
}
if (product.category === "clothing") {
return 0.15;
}
return 0.05;
}
function calculatePrice(product, user) {
const discount = getDiscount(product, user);
return product.price * (1 - discount);
}The total business logic hasn't disappeared.
We've simply separated responsibilities.
That makes each piece easier to understand and test.
Techniques for Reducing Cyclomatic Complexity
There are several techniques that can help.
1. Extract Functions
Instead of putting everything inside one function:
function processOrder(order) {
// 100 lines of logic...
}Split related logic into smaller functions:
function validateOrder(order) {
// validation
}
function calculateTotal(order) {
// pricing
}
function processPayment(order) {
// payment
}Each function can now have a smaller and more focused control-flow graph.
2. Use Guard Clauses
Deeply nested conditions can often be replaced with early returns.
Instead of:
function processUser(user) {
if (user) {
if (user.active) {
if (user.email) {
return "Process";
}
}
}
return "Invalid";
}You can write:
function processUser(user) {
if (!user) return "Invalid";
if (!user.active) return "Invalid";
if (!user.email) return "Invalid";
return "Process";
}The complexity hasn't necessarily disappeared mathematically, but the code is easier to read.
This distinction is important:
Reducing cyclomatic complexity and improving readability are related, but they aren't exactly the same thing.
3. Replace Large Conditional Chains
Consider:
function getMessage(status) {
if (status === "pending") {
return "Waiting";
} else if (status === "approved") {
return "Approved";
} else if (status === "rejected") {
return "Rejected";
} else if (status === "cancelled") {
return "Cancelled";
}
return "Unknown";
}Depending on the situation, a lookup object may be clearer:
const messages = {
pending: "Waiting",
approved: "Approved",
rejected: "Rejected",
cancelled: "Cancelled"
};
function getMessage(status) {
return messages[status] ?? "Unknown";
}Now the function has much less control-flow complexity.
However, don't blindly replace every if with an object. If each branch contains different business logic, the conditional structure may be the clearer choice.
Cyclomatic Complexity Isn't the Same as Cognitive Complexity
These metrics are related, but they measure different things.
Cyclomatic complexity focuses primarily on the number of independent control-flow paths.
Cognitive complexity attempts to measure how difficult code is for a human to understand.
Consider:
if (user) {
if (user.account) {
if (user.account.active) {
// ...
}
}
}This may not have an enormous cyclomatic complexity, but the nesting makes the code harder to mentally parse.
That's where cognitive complexity can provide additional information.
Modern static-analysis tools often provide both metrics.
Cyclomatic Complexity in Real Projects
You don't normally calculate cyclomatic complexity manually in a professional codebase.
Static-analysis tools can calculate it automatically.
For JavaScript and TypeScript projects, tools such as ESLint plugins and code-quality platforms can report complexity for individual functions.
For example, an ESLint configuration can enforce a maximum complexity:
{
"rules": {
"complexity": ["error", 10]
}
}This tells ESLint to report functions whose cyclomatic complexity exceeds the configured threshold.
The exact configuration should depend on your project's needs.
A strict threshold can be useful, but setting it arbitrarily low can also become annoying.
Should You Always Reduce Cyclomatic Complexity?
No.
This is probably the most important part of the entire topic.
Cyclomatic complexity is a signal, not a verdict.
Suppose you have a function with a complexity of 12.
That doesn't automatically mean:
BAD CODEYou should ask:
- Is the function easy to understand?
- Are the branches logically related?
- Is the code well tested?
- Does the function have a clear responsibility?
- Are the conditions likely to change?
- Is the complexity inherent to the problem?
- Would splitting the function actually make the code clearer?
Sometimes a function with a higher complexity is perfectly reasonable.
The goal isn't:
"Make the complexity number as small as possible."
The better goal is:
Keep control flow understandable, testable, and appropriate for the responsibility of the function.
A Practical Example
Let's look at a realistic example.
Suppose you're validating a signup request:
function validateSignup(data) {
if (!data.email) {
return "Email is required";
}
if (!data.password) {
return "Password is required";
}
if (data.password.length < 8) {
return "Password is too short";
}
if (data.age < 18) {
return "You must be at least 18";
}
if (!data.termsAccepted) {
return "Accept the terms";
}
return "Valid";
}There are five decisions.
So the basic cyclomatic complexity is:
5 + 1 = 6That's not necessarily a problem.
In fact, the function is arguably quite readable.
Trying to reduce its complexity just for the sake of reducing the number could make it worse.
For example, creating five tiny functions for five simple conditions might technically alter where complexity is measured, but it could make the overall design unnecessarily fragmented.
This is why metrics should support engineering judgment rather than replace it.
The Relationship Between Complexity and Bugs
High complexity doesn't directly cause bugs.
However, complex control flow creates more opportunities for developers to overlook a scenario.
Imagine:
if (A) {
if (B) {
if (C) {
if (D) {
// ...
}
}
}
}As conditions increase, the number of possible combinations can grow rapidly.
For four independent boolean conditions, there are:
2⁴ = 16possible combinations.
For ten:
2¹⁰ = 1024possible combinations.
You don't necessarily need one test for every combination—that depends on the logic and testing strategy—but this illustrates why complicated conditional logic can become difficult to reason about.
Cyclomatic complexity gives you a simpler way to detect that branching structure is increasing.
Cyclomatic Complexity in Code Review
When reviewing code, don't treat complexity as a checkbox.
Instead, use it as a conversation starter.
If you see a function with high complexity, ask:
"Can these branches be separated into independent responsibilities?"
Or:
"Is there a simpler way to represent this state?"
Or:
"Do we have tests covering the important paths?"
For example, this:
function handleRequest(request) {
// authentication
// authorization
// validation
// business rules
// database operations
// notifications
// logging
}may be a stronger refactoring candidate than a function that happens to have a slightly higher complexity but performs one well-defined task.
Key Takeaways
Cyclomatic complexity measures the number of independent paths through code.
The basic idea is straightforward:
Cyclomatic Complexity
= Decision Points + 1For a simple function:
function example(value) {
if (value > 10) {
return true;
}
return false;
}There is one decision, so:
Complexity = 2As you add more branches, loops, and conditions, the complexity increases.
High cyclomatic complexity can indicate code that deserves additional attention because it may be:
- Harder to understand
- Harder to test
- Harder to modify
- More difficult to review
But complexity isn't inherently bad.
A higher number doesn't automatically mean you need to refactor.
The most useful way to think about cyclomatic complexity is as a warning signal that helps you identify potentially complicated control flow.
Use it alongside:
- Unit tests
- Code review
- Cognitive complexity
- Maintainability
- Function size
- Architecture
- Domain complexity
Rather than optimizing for a particular number, optimize for code that another developer can understand and safely change six months from now.
And honestly, that's probably a much better definition of "clean code" than any metric can provide.
Frequently Asked Questions
What is cyclomatic complexity in simple terms?
Cyclomatic complexity measures how many independent paths exist through a piece of code. More decision points generally mean higher complexity.
What is the formula for cyclomatic complexity?
The graph-based formula is:
M = E - N + 2PFor a single connected component, this becomes:
M = E - N + 2A simpler practical approach is:
Decision points + 1What is a good cyclomatic complexity?
There is no universal ideal number. A commonly used rule of thumb considers 1–5 low, 6–10 moderate, and values above 10 increasingly worthy of review. The appropriate threshold depends on the project and type of code.
Does high cyclomatic complexity mean bad code?
No. It indicates more complicated control flow, but complexity must be evaluated in context. Some domains naturally require more branching.
Does cyclomatic complexity determine the number of tests?
It can provide an approximation of the number of independent paths that should be considered for basic path coverage. It does not determine a complete test suite because real-world testing also depends on input combinations, business rules, boundaries, integrations, and other factors.
How can I reduce cyclomatic complexity?
Common approaches include extracting functions, simplifying conditional logic, using guard clauses, replacing large conditional mappings with data structures where appropriate, and separating different responsibilities.
Final Thought
Cyclomatic complexity is one of those metrics that can be either useful or completely meaningless depending on how you use it.
If you're using it to say:
"This function has a complexity of 15, therefore it's bad."
you're missing the point.
If you're using it to notice:
"This function has accumulated 15 independent paths. Let's look at whether all of this logic really belongs here."
then the metric becomes genuinely useful.
The number isn't the goal.
Understanding and maintaining the code is.