BFS vs DFS: Differences, Examples in JavaScript & Python, When to Use Each

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

·
6 min read
Published Mar 7, 2025
Updated Oct 9, 2026
BFS vs. DFS: Key Differences & Best Use Cases in Graphs Banner Image
14 min read
943 words
20 sections15 code blocks

BFS (breadth-first search) explores a graph level by level using a queue, so it finds the shortest path in an unweighted graph. DFS (depth-first search) follows one branch as deep as it can before backtracking, using a stack or recursion, which suits cycle detection, topological sorting and backtracking puzzles. Both visit every node and edge once: O(V + E) time.

Tested with: Node.js 20.20 and Python 3.14, October 2026. All outputs and timings below come from those runs.

BFS vs DFS at a Glance

BFSDFS
OrderLevel by level (closest nodes first)One branch to the end, then backtrack
Data structureQueue (first in, first out)Stack (last in, first out) or recursion
Shortest path (unweighted)Yes, guaranteedNo
TimeO(V + E)O(V + E)
Extra memoryO(V); in practice the widest levelO(V); in practice the deepest path
Good forShortest path, nearest match, levels, friend-of-friendCycles, topological sort, connected components, mazes, backtracking
Watch out forMemory on very wide graphsStack overflow when recursion is deep

V is the number of nodes (vertices) and E the number of edges. New to graphs? Start with our graph data structures guide.

The Example Graph

JavaScript
// Adjacency list: each node maps to the nodes it points to
export const graph = {
  A: ["B", "C"],
  B: ["D", "E"],
  C: ["F"],
  D: [],
  E: ["F"],
  F: [],
};
Bash
      A
     / \
    B   C
   / \   \
  D   E → F      (E and C both point to F)

BFS from A: A B C D E F   (level by level)
DFS from A: A B D E F C   (one branch to the bottom, then back up)

Breadth-First Search (BFS)

Take the first node from the queue, record it, and add its unvisited neighbors to the back. Mark a node visited when you add it to the queue, not when you take it out:

JavaScript
// Breadth-first search: visit nodes level by level
export function bfs(graph, start) {
  const visited = new Set([start]);
  const queue = [start];
  const order = [];

  // Read with an index instead of queue.shift(), which is O(n) per call
  for (let head = 0; head < queue.length; head++) {
    const node = queue[head];
    order.push(node);
    for (const next of graph[node] ?? []) {
      if (!visited.has(next)) {
        visited.add(next); // mark when queued, so nothing is queued twice
        queue.push(next);
      }
    }
  }
  return order;
}
JavaScript
import { graph } from "./graph.js";
import { bfs } from "./bfs.js";

console.log(bfs(graph, "A")); // ["A", "B", "C", "D", "E", "F"]

Two bugs in the usual BFS

JavaScript
// A common version (including an older copy of this article):
while (queue.length > 0) {
  const node = queue.shift();       // O(n): moves every remaining item
  if (!visited.has(node)) {
    visited.add(node);              // marked only when dequeued...
    queue.push(...graph[node]);     // ...so F gets queued twice
  }
}
  • Marking on dequeue queues nodes twice. On this 6-node graph the old version queued 7 nodes (F twice). On dense graphs the waste is much larger.
  • queue.shift() is slow. It moves every remaining item. On a node with 100,000 neighbors, the shift() version took 933 ms; the head-index version took 38 ms (20,000: 35 ms vs 8 ms).
  • push(...array) can crash. Spreading 200,000 neighbors into push() threw RangeError: Maximum call stack size exceeded, because each item becomes a function argument. A loop doesn't have that limit.

Shortest path with BFS

Because BFS reaches nodes in order of distance, the first time it reaches the goal is via a shortest path. Remember each node's parent to rebuild it:

JavaScript
// Fewest-edges path from start to goal (unweighted graph), or null
export function shortestPath(graph, start, goal) {
  const parent = new Map([[start, null]]);
  const queue = [start];

  for (let head = 0; head < queue.length; head++) {
    const node = queue[head];
    if (node === goal) {
      const path = [];
      for (let at = goal; at !== null; at = parent.get(at)) path.push(at);
      return path.reverse();
    }
    for (const next of graph[node] ?? []) {
      if (!parent.has(next)) {
        parent.set(next, node);
        queue.push(next);
      }
    }
  }
  return null; // goal can't be reached
}
JavaScript
shortestPath(graph, "A", "F"); // ["A", "C", "F"]  (2 edges; A→B→E→F is 3)
shortestPath(graph, "D", "A"); // null  (edges only go one way)

This counts edges, not distances. When edges have weights (road lengths, costs), use Dijkstra's algorithm, or Bellman-Ford if weights can be negative.

BFS on a grid: fewest steps through a maze

JavaScript
// Fewest steps through a maze: "#" is a wall, moves are up/down/left/right
export function mazeSteps(maze, [startRow, startCol], [endRow, endCol]) {
  const rows = maze.length;
  const cols = maze[0].length;
  const seen = new Set([`${startRow},${startCol}`]);
  const queue = [[startRow, startCol, 0]];

  for (let head = 0; head < queue.length; head++) {
    const [r, c, steps] = queue[head];
    if (r === endRow && c === endCol) return steps;
    for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
      const nr = r + dr;
      const nc = c + dc;
      const key = `${nr},${nc}`;
      if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue;
      if (maze[nr][nc] === "#" || seen.has(key)) continue;
      seen.add(key);
      queue.push([nr, nc, steps + 1]);
    }
  }
  return -1; // no way through
}
JavaScript
const maze = [
  "S..#....",
  ".#.#.##.",
  ".#...#..",
  ".####.#.",
  "......#E",
];
mazeSteps(maze, [0, 0], [4, 7]);          // 15
mazeSteps(["S#", "#E"], [0, 0], [1, 1]);  // -1 (walled off)

Grids are graphs too: each open cell is a node and its four neighbors are edges. This is the pattern behind many "minimum moves" interview questions.

Depth-First Search (DFS)

The recursive version is the easiest to read. The stack version does the same work without using the call stack:

JavaScript
// Depth-first search, recursive: short and clear, but limited by the call stack
export function dfsRecursive(graph, node, visited = new Set(), order = []) {
  visited.add(node);
  order.push(node);
  for (const next of graph[node] ?? []) {
    if (!visited.has(next)) dfsRecursive(graph, next, visited, order);
  }
  return order;
}

// Depth-first search with an explicit stack: same order, no recursion limit
export function dfs(graph, start) {
  const visited = new Set();
  const stack = [start];
  const order = [];

  while (stack.length > 0) {
    const node = stack.pop();
    if (visited.has(node)) continue;
    visited.add(node);
    order.push(node);
    // Push in reverse so the first neighbor is explored first
    const neighbors = graph[node] ?? [];
    for (let i = neighbors.length - 1; i >= 0; i--) {
      if (!visited.has(neighbors[i])) stack.push(neighbors[i]);
    }
  }
  return order;
}
JavaScript
dfs(graph, "A");          // ["A", "B", "D", "E", "F", "C"]
dfsRecursive(graph, "A"); // ["A", "B", "D", "E", "F", "C"]

Both returned the same order in our test. The stack version pushes neighbors in reverse so the first neighbor is explored first; without that, it still works but visits branches right to left.

Recursion depth limits

  • Node.js allowed about 11,400 nested calls in our test. Recursive DFS on a 100,000-node chain threw RangeError: Maximum call stack size exceeded; the stack version visited all 100,000 nodes.
  • Python's default limit is 1,000. A 5,000-node chain raised RecursionError: maximum recursion depth exceeded; the iterative version worked.

Use recursion for trees and small graphs; switch to an explicit stack when depth can grow with the input. More in our recursion guide.

Cycle detection with DFS

In a directed graph, a cycle exists if DFS reaches a node that is still on the current path:

JavaScript
// Cycle detection in a directed graph with DFS.
// "visiting" = on the current path; reaching it again means a cycle.
export function hasCycle(graph) {
  const state = new Map(); // node -> "visiting" | "done"

  function visit(node) {
    if (state.get(node) === "visiting") return true;
    if (state.get(node) === "done") return false;
    state.set(node, "visiting");
    for (const next of graph[node] ?? []) {
      if (visit(next)) return true;
    }
    state.set(node, "done");
    return false;
  }

  return Object.keys(graph).some((node) => visit(node));
}
JavaScript
hasCycle(graph);                  // false
hasCycle({ ...graph, F: ["B"] });  // true: B → E → F → B

A plain "visited" set isn't enough for directed graphs: in our example graph F is reached twice (via C and via E) without any cycle. The "visiting" state is what tells the two cases apart. The same DFS, recording nodes as they finish, gives a topological order (for build steps, course prerequisites or task scheduling).

BFS and DFS in Python

Python
from collections import deque

graph = {
    "A": ["B", "C"],
    "B": ["D", "E"],
    "C": ["F"],
    "D": [],
    "E": ["F"],
    "F": [],
}


def bfs(graph, start):
    visited = {start}
    queue = deque([start])  # popleft() is O(1); list.pop(0) is O(n)
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for nxt in graph.get(node, []):
            if nxt not in visited:
                visited.add(nxt)
                queue.append(nxt)
    return order


def dfs(graph, start):
    visited = set()
    stack = [start]
    order = []
    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)
        order.append(node)
        for nxt in reversed(graph.get(node, [])):
            if nxt not in visited:
                stack.append(nxt)
    return order


def dfs_recursive(graph, node, visited=None):
    if visited is None:  # never use visited=set() as a default
        visited = set()
    visited.add(node)
    order = [node]
    for nxt in graph.get(node, []):
        if nxt not in visited:
            order += dfs_recursive(graph, nxt, visited)
    return order


print(bfs(graph, "A"))            # ['A', 'B', 'C', 'D', 'E', 'F']
print(dfs(graph, "A"))            # ['A', 'B', 'D', 'E', 'F', 'C']
print(dfs_recursive(graph, "A"))  # ['A', 'B', 'D', 'E', 'F', 'C']

All three printed the orders shown in the comments. Use collections.deque for the queue: list.pop(0) has the same O(n) problem as shift().

Python
# Don't do this: the default set is created once and shared by every call
def dfs(graph, node, visited=set()):
    ...

dfs(graph, "A")  # prints A B D E F C
dfs(graph, "A")  # prints nothing: every node is already "visited"

We ran this: the second call printed nothing. Python evaluates default arguments once, so every call shares the same set. Use visited=None as in dfs_recursive above.

When to Use BFS vs DFS

ProblemUseWhy
Shortest path / fewest moves (unweighted)BFSReaches nodes in order of distance
People within 2 connections, nearby itemsBFSStops after the levels you need
Web crawler limited to N clicks from the homepageBFSCrawls by depth level
Is there any path / are these connected?EitherBoth visit everything reachable
Cycle detection, dependency orderDFSTracks the current path
Sudoku, N-Queens, permutationsDFS (backtracking)Tries one choice fully, then undoes it
Very deep graphIterative DFS or BFSRecursion would overflow
Very wide graph, answer is deepDFSBFS would hold a huge level in memory

Backtracking is DFS over choices; see backtracking with N-Queens and Sudoku. For minimum spanning trees, see Kruskal's algorithm.

BFS and DFS in AI (Search Problems)

In AI courses, BFS and DFS are uninformed search strategies: they explore a state space (puzzle positions, game states, routes) without a heuristic. With branching factor b, solution depth d and maximum depth m:

BFSDFSIterative deepening DFS
Complete (always finds a solution if one exists)Yes (finite b)No (can go down an infinite branch)Yes
Optimal (fewest steps)Yes, if every step costs the sameNoYes, if every step costs the same
TimeO(b^d)O(b^m)O(b^d)
MemoryO(b^d)O(b·m)O(b·d)

Iterative deepening runs DFS with a depth limit of 1, 2, 3… It keeps DFS's small memory and BFS's guarantee of the shallowest solution, which is why it's the usual choice for large search spaces. When you have a good estimate of the distance to the goal, informed search such as A* beats all three. See Big-O notation if the complexity notation is new.

Common Mistakes

MistakeWhat happensFix
Marking visited when dequeuingNodes queued several timesMark when you enqueue
queue.shift() / list.pop(0)O(n²) on big graphsHead index, or deque in Python
queue.push(...neighbors)RangeError with very many neighborsPush in a loop
Recursive DFS on deep inputStack overflow / RecursionErrorExplicit stack
visited=set() default in PythonSecond call returns nothingvisited=None, create inside
Using BFS for weighted shortest pathsFewest edges, not lowest costDijkstra
Plain visited set for directed cyclesFalse positives (F reached twice)Track nodes on the current path

FAQs

What is the main difference between BFS and DFS?

BFS visits nodes in order of distance from the start, using a queue. DFS goes as deep as possible along one branch before backtracking, using a stack or recursion.

Which is faster, BFS or DFS?

Both are O(V + E) when implemented correctly. Which one finds your answer sooner depends on where it is: near the start favors BFS, deep in one branch favors DFS.

Why does BFS find the shortest path?

It processes every node at distance 1 before any at distance 2, and so on. The first time it reaches the goal, no shorter path can exist. This holds only when all edges count the same.

Which uses more memory?

BFS stores a whole level at once, so it uses more on wide graphs. DFS stores the current path, so it uses more on long, deep graphs. Worst case, both are O(V).

Is DFS always recursive?

No. Any recursive DFS can use an explicit stack instead, which avoids stack-overflow errors on deep graphs.

What are BFS and DFS used for in real life?

BFS: route finding with equal step costs, social "people you may know", network broadcast, level-limited crawling. DFS: dependency resolution and build order, detecting deadlocks (cycles), maze generation, solving puzzles.

No comments yet. Be the first to comment!

Leave a Comment

2000 characters remaining

Related Articles

View all
Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.