Queue and Stack in C# make consumption order explicit. A queue removes the oldest remaining item first. A stack removes the most recently added remaining item first. Both can hold duplicate values, and both are mutable collections.
That sounds simple, but the distinction becomes useful when application code handles pending requests, recent navigation, or reversible actions. A collection's order can explain the next item to consume without a convention about which list index to remove.
This guide stays with ordinary Queue<T> and Stack<T> behavior, including empty-state handling and copied views. It does not build a concurrent producer-consumer system, an async pipeline, or a durable job queue. Examples use C# 12-compatible syntax and stable .NET 10 APIs; they are documented, not executed.
Queue and Stack in C# Express a Removal Policy
FIFO means first in, first out. Among items still in a queue, the earliest enqueued item is the next one removed. LIFO means last in, first out. Among items still in a stack, the latest pushed item is the next one removed.
These are collection-level ordering rules. They do not say when an item finishes processing, whether a failed operation retries, or whether data survives a process restart.
The stable .NET 10 Queue implementation describes a first-in, first-out collection. The stable Stack implementation returns the top element on Pop.
| Operation | Queue meaning | Stack meaning |
|---|---|---|
| Add an item | Enqueue: add at the tail |
Push: add at the top |
| Remove the next item | Dequeue: remove from the head |
Pop: remove from the top |
| Inspect without removal | Peek: read the head |
Peek: read the top |
| Remove if available | TryDequeue |
TryPop |
| Inspect if available | TryPeek |
TryPeek |
| Count remaining members | Count |
Count |
The operations communicate intent more directly than “remove index zero” or “remove the last list item.” That clarity is valuable even before considering implementation costs.
A Queue Preserves Arrival Order for Consumption
Imagine requests received by a single-threaded application step. The application wants to handle them in the order they were added. A queue expresses that requirement directly.
using System;
using System.Collections.Generic;
var pendingRequests = new Queue<string>();
pendingRequests.Enqueue("Request-A");
pendingRequests.Enqueue("Request-B");
pendingRequests.Enqueue("Request-C");
Console.WriteLine($"Next: {pendingRequests.Peek()}");
Console.WriteLine($"Before removal: {pendingRequests.Count}");
while (pendingRequests.TryDequeue(out var request))
{
Console.WriteLine($"Handling {request}");
}
Console.WriteLine($"Remaining: {pendingRequests.Count}");
The peek does not remove the first request. The loop removes one available request at a time and stops when no member remains. The consumed sequence follows the enqueue sequence in this single-threaded example.
The printed “Handling” message is not evidence of successful business processing. It simply shows where processing would occur. A real application must define failure handling separately from removal.
If handling fails after removal, the queue does not automatically put the request back. Re-enqueueing it would place it at the tail, which changes its relationship to other pending work. That can be appropriate or inappropriate depending on the application policy.
For retry logic, describe the policy rather than assuming FIFO answers it. “Oldest pending request first” and “failed request retries immediately” can conflict. A collection can implement one part of the policy while the application decides the other.
A Stack Preserves Recent-First Consumption
A stack is useful when the latest entry should be handled before earlier entries. Reversing recently recorded actions is a common example, although the collection itself does not implement those actions.
using System;
using System.Collections.Generic;
var history = new Stack<string>();
history.Push("Opened document");
history.Push("Changed heading");
history.Push("Added paragraph");
Console.WriteLine($"Latest: {history.Peek()}");
while (history.TryPop(out var action))
{
Console.WriteLine($"Reviewing {action}");
}
The most recently pushed action is removed first. Earlier actions remain below it until later pops. The strings are descriptions, not executable undo commands, so this example demonstrates order without pretending to reverse a document.
A real undo system requires enough information to reverse each action. It also needs rules around redo, grouped edits, and new actions recorded after an undo. A stack provides the recent-first ordering but not those semantics.
This boundary is similar to the distinction between keyed data access in a Dictionary and the meaning of the values stored there. A container expresses access and order; application behavior gives the stored values their role.
If the next item should be chosen by priority rather than recency, the stack contract does not match the requirement. Pushing an urgent item can make it next temporarily, but that does not establish a general priority policy for all entries.
Empty State Is an Expected Outcome
An empty queue or stack is often normal. An application may have no pending requests or no recorded history. When absence is expected, the try operations make that outcome explicit.
Microsoft documents TryDequeue as returning false when the queue is empty. Microsoft documents TryPop as returning false when the stack is empty.
using System;
using System.Collections.Generic;
var measurements = new Queue<int>();
measurements.Enqueue(0);
if (measurements.TryDequeue(out var measurement))
{
Console.WriteLine($"Received measurement: {measurement}");
}
else
{
Console.WriteLine("No measurement is available.");
}
if (measurements.TryDequeue(out var nextMeasurement))
{
Console.WriteLine($"Received measurement: {nextMeasurement}");
}
else
{
Console.WriteLine("The queue is now empty.");
}
The Boolean result distinguishes a real zero from absence. Looking only at the output variable's value would be incorrect because zero is a valid queued integer and is also the default value of int.
The same issue exists for nullable references. A valid null element, where the element type allows it, is not the same as a failed removal. The success result establishes whether an element was obtained.
Dequeue, Pop, and Peek use exceptions for their empty-state boundary. Those APIs can be appropriate when the application has an invariant that an item must exist. If empty is routine, try operations avoid expressing normal absence as an exceptional path.
Do not swallow an empty-state error and invent a successful-looking item. The application should either handle absence explicitly or surface a violated invariant through its established error policy.
Peek Observes Without Consuming
Both types offer Peek and TryPeek. They identify the next member without removing it. That is useful for previews, labels, or decisions that do not yet commit to consumption.
Peeking is not reserving. In a shared-access design, observing an item does not establish that a later remove operation returns the same item. Another participant may intervene unless a surrounding synchronization policy prevents it.
Even in single-threaded code, another operation called between peek and remove may change the structure. The two calls remain separate operations. If the application needs to make a decision and consume under one invariant, the workflow must keep those actions coordinated.
The stable Queue and Stack source shows that try-peek returns a value without updating the member count. Removal methods update that count. Reading and consuming are intentionally separate behaviors.
A preview also does not imply processing ownership. Displaying the oldest request on a screen should not prevent a separate operation from handling it unless the application explicitly introduces that reservation policy.
Duplicate Values Are Still Separate Members
Neither an ordinary queue nor an ordinary stack is a set. Enqueueing or pushing an equal value twice records two members. Two equal strings can therefore be removed twice.
This is often desirable. Repeated clicks or repeated event descriptions can represent different occurrences. Equality of the stored value does not prove the application should merge those occurrences.
If duplicate work must be prevented, introduce a distinct identity and membership policy. A set of known identifiers might participate, but maintaining that set alongside a queue creates a compound workflow that must remain consistent.
The Dictionary selection primer gives context for associating state with identifiers. The question is whether repeated identifiers mean duplicate work, separate attempts, or updated information.
A simple queue tutorial should not hide this design issue by adding a Contains call before every enqueue and claiming the problem is solved. That check and the later enqueue are separate actions, and equality may not match the real work identity.
Enumeration and Removal Are Different
Enumerating a queue or stack reads its elements through an enumerator. It is not the same as repeatedly dequeuing or popping. Removal changes membership, while traversal does not inherently consume members.
The IEnumerable guide explains the traversal surface. A queue or stack implements that surface in addition to its consumption operations.
The stable .NET 10 implementations maintain version information for their enumerators. Do not dequeue or pop inside a traversal of the same ordinary collection and assume the loop is a mutation-safe snapshot.
At the same time, do not apply that explanation as a universal rule for every collection. A foreach loop uses the source's actual enumeration contract. Concurrent collections may have different traversal semantics.
If the goal is to consume all current members, a try-removal loop communicates that goal. If the goal is to inspect without consuming, enumerate without structural changes or acquire an appropriate copied view.
Copies Preserve a View, Not Every Ownership Guarantee
Queue and Stack provide ToArray methods. In the stable .NET 10 implementations, the queue copy uses dequeue order and the stack copy uses pop order. Those are useful inspection representations.
using System;
using System.Collections.Generic;
var queue = new Queue<string>(new[] { "A", "B", "C" });
var stack = new Stack<string>(new[] { "A", "B", "C" });
var queueView = queue.ToArray();
var stackView = stack.ToArray();
queue.Dequeue();
stack.Pop();
Console.WriteLine($"Queue copy: {string.Join(", ", queueView)}");
Console.WriteLine($"Stack copy: {string.Join(", ", stackView)}");
Console.WriteLine($"Remaining queue members: {queue.Count}");
Console.WriteLine($"Remaining stack members: {stack.Count}");
The copied arrays retain their captured members when the original collections remove members. The stack constructor pushes values in source traversal order, making the final supplied value the top. The array copy reflects that top-first view.
A subtle boundary follows: constructing another stack directly from a top-first array pushes those values in that array's order, which can reverse the next-pop order relative to the original. A view's order and a constructor's insertion behavior must be considered together.
These copies are shallow. Strings make the example simple, but mutable referenced objects could remain shared. Copying a queue of mutable jobs does not freeze each job's internal state.
Acquisition also matters. Ordinary collections do not gain concurrent snapshot guarantees merely because they have ToArray. If writers can overlap with copying, coordinate the acquisition.
A Collection Is Not a Durable Workflow
FIFO does not imply exactly-once processing. LIFO does not imply a complete undo system. Neither implies durability or coordinated asynchronous waiting.
For a single-threaded local operation, that limited scope is often an advantage. The collection is small, understandable, and does not pretend to solve concerns outside its contract.
For shared processing, identify whether waiting, completion, bounded capacity, cancellation, and acknowledgements are required. Those are separate coordination concerns and intentionally outside this article's examples.
The iterator and collection traps illustrate why consumption behavior needs careful thought. Here the key lesson is simpler: removing a member and successfully completing its associated business operation are different events.
Read a Consumption Loop as a State Change
A try-removal loop can look like ordinary iteration, but each successful condition changes collection membership. That difference matters when the loop body calls other application code.
If the body throws, the removed member is already outside the collection. Remaining members are still present, but the application needs a policy for the failed member. That policy might record a failure, stop the operation, or arrange another attempt. It does not follow automatically from the loop syntax.
For a stack representing reversible work, popping before reversal raises the same question. If reversal fails, should the action remain in history? The stack does not answer that. The surrounding operation needs to decide how history and application state stay consistent.
This does not mean ordinary Queue and Stack are unsuitable. It means they provide an intentionally limited contract. A small single-threaded operation may need only that contract, while a durable workflow needs additional guarantees.
The same distinction applies to completion. An empty queue at one instant means no member is currently available. If another part of the application can enqueue later, emptiness does not mean the producer has permanently finished. Completion must be represented by a separate policy.
Order should also be described precisely in examples and documentation. “Requests are removed in arrival order” is a collection statement. “Requests finish in arrival order” includes the processing behavior. A collection tutorial should not substitute the first as proof of the second.
For stack-based navigation history, “most recently recorded entry is next” is similarly narrower than “navigation always returns to the correct previous screen.” The entries must contain the right state, and the application must apply them correctly.
Reading the collection operation as a state change makes these boundaries easier to see. It keeps the example useful without quietly turning an in-memory container into a workflow engine.
Frequently Asked Questions
What is the difference between FIFO and LIFO?
FIFO removes the oldest remaining entry first, as a queue does. LIFO removes the newest remaining entry first, as a stack does. Both describe removal order, not the timing of business-operation completion.
Does Peek remove an item?
No. It reads the next queue member or top stack member without consuming it. A later removal is a separate operation and can require coordination if other code can change the collection.
What happens when TryDequeue or TryPop sees an empty collection?
It returns false. Use that result to handle absence explicitly. Do not infer success from the output value because default values such as zero can also be legitimate stored members.
Can Queue and Stack contain duplicates?
Yes. Equal values can be stored as separate members. Whether repeated values should be merged is an application identity policy, not a FIFO or LIFO rule.
Does foreach consume the collection?
No. Enumeration is distinct from removal. Use a try-removal loop to consume members, and follow the particular collection's enumeration contract when inspecting it.
Is Queue a complete background-processing system?
No. It supplies an in-memory FIFO collection. Durability, retry, acknowledgement, waiting, and coordination require surrounding behavior and are not established by enqueueing an item.
Understanding Queue and Stack in C#
A queue makes oldest-first consumption explicit. A stack makes newest-first consumption explicit. Peek separates observation from removal, and try operations make expected absence visible.
Those small contracts are useful precisely because they are specific. Keeping collection order separate from processing, ownership, and coordination makes application code easier to reason about.




