AlgoMaster Logo

Return Types & Tuples

Last Updated: May 22, 2026

High Priority
11 min read

A method's return type is the contract it offers to callers: what shape of value will come back when the method finishes. C# is strict about this contract, the compiler refuses to build a method that promises an int and forgets to return one on some code path. This lesson walks through what can be returned, how return actually behaves, and how tuples let a single method hand back several related values at once without inventing a class for the job.

What a Method Can Return

Any C# type can sit on the left of a method name as its return type. Primitives, strings, arrays, lists, dictionaries, your own classes, structs, enums, delegates, tuples, even other methods (wrapped in a delegate). There's no special category of "returnable" types, the language treats them all the same way.

A handful of common shapes from an e-commerce setting:

Each of those methods declares one return type and hands one value back. The caller gets exactly that type and can use it however that type allows: arithmetic on a decimal, indexing on an array, iteration on a List<T>, and so on.

The methods in that block use => (expression-bodied syntax) because each body is a single expression. That style was introduced in C# 6 and is the standard form for one-line methods. The longer form with braces and an explicit return keyword is covered later in this lesson.

Arrays, lists, dictionaries, and custom classes are reference types, so what actually comes back is a reference, not a copy of the data. That detail matters when the caller mutates the result, every change is visible to anyone else holding the same reference.

void: A Method That Returns Nothing

void is the one return type that isn't a real type. It's the keyword you use when a method doesn't have anything meaningful to give back. Its job is the side effect: printing, modifying state, sending a network request.

Two rules apply inside a void method:

  • You can use return; (bare, no value) to exit early.
  • You cannot use return someValue;, because there's no return type to match.

That bare return is useful for guard clauses:

The return; inside the if block stops execution at that point. Anything after the guard is skipped when the email is missing. This is the same return keyword used by value-returning methods, just without an expression after it.

This lesson assumes you've already met void in the Methods Basics chapter. Everything that follows focuses on methods that do return a value.

The return Keyword

return does two things at the same time:

  1. It evaluates the expression that follows it (if any) and hands the result back to the caller.
  2. It terminates the method immediately. No code after return in the same execution path runs.

A minimal value-returning method:

The expression after return can be anything that evaluates to the declared type. A literal, a variable, a method call, an arithmetic expression, a conditional expression, all valid:

The return type and the value handed to return have to be compatible. Hand back something the compiler can't convert to the declared type and the build fails:

What's wrong with this code?

The method promises a decimal, but the literal "89.97" is a string. The compiler reports error CS0029 ("Cannot implicitly convert type 'string' to 'decimal'") and refuses to build. The fix is either to return the right type (return 89.97m;) or to parse the string explicitly with decimal.Parse("89.97") if the value really has to start as a string.

The compatibility check uses implicit conversions. Returning an int from a method declared decimal works because there's an implicit int to decimal conversion. Returning a double from a decimal-returning method doesn't, because double to decimal requires an explicit cast.

Multiple Return Paths

Most non-trivial methods have more than one place where return shows up. A method that classifies orders, validates input, or short-circuits on edge cases needs several exit points.

The compiler enforces one rule across all of them: every reachable code path has to return a value of the declared type. If even one path falls off the end without returning, the build fails.

A discount calculator with a few branches:

Each if block has its own return for the matching tier. The final return 0m; outside all the if blocks is the fallback for carts below $50. Drop that last line and the compiler complains:

What's wrong with this code?

CS0161 ("not all code paths return a value"). When the cart total is below $50, none of the three if conditions match, and the method runs to the closing brace without ever hitting a return. The fix is the same as before: add a final return 0m; for the missing case. Some teams prefer to write that fallback as the body of an else, others as a bare statement at the end. Both compile.

The compiler is conservative here. It doesn't try to prove that one of the branches is guaranteed to fire, it just checks the control-flow graph. Even when you, the human, can see that every input must hit one of the branches, the compiler often can't. Add the explicit fallback and move on.

Early-Return Pattern

The classic alternative to a deeply nested if chain is to validate inputs first and bail out early. The "happy path" then lives flush against the left margin, easy to read.

A validation method that fails fast:

Each guard handles one bad case and exits. The method's tail runs only when everything is valid. The alternative, a single nested if with three levels of indentation, is harder to read and harder to extend. The early-return style stays flat as more rules get added.

return in Loops

return inside a loop exits the entire method on the spot, not just the loop. That's useful when you're searching for a match and want to stop as soon as you find it.

The loop runs only until it hits the first item with stock[i] == 0. The return names[i]; ends the method immediately, so iterations beyond Keyboard never run. Falling out of the loop means nothing matched, and the final return null; reports that case.

The return type here is string?, the nullable string. The ? says "this method might return null," which lets the compiler check the caller for missing null handling. Without the ?, the line return null; would warn under nullable reference types (enabled by default in .NET 6+ project templates).

Expression-Bodied Methods

When a method's whole body is a single expression, the curly braces and the return keyword are noise. C# 6 introduced => to let you write that case in one line.

The => form is shorthand. The compiler generates the same IL it would for the equivalent block body, so there's no runtime difference. The only restriction is that the body must be a single expression, you can't put multiple statements, an if/else, or a loop after the =>.

When to pick which form:

Method bodyRecommended form
One expression, fits on one lineExpression-bodied (=>)
One expression, but reads better split across linesExpression-bodied with line break before =>
Multiple statements, conditionals, or loopsBlock-bodied with { }
Method always returns the same constantExpression-bodied

A common mistake is reaching for => when the body has grown beyond a single expression and contorting the code to fit. If you find yourself nesting ternary operators three deep to keep the => form, that's a sign to go back to a block body. Readability wins over brevity.

The Methods Basics chapter showed => as one syntax option. This lesson uses it heavily: most return-anything-meaningful methods that are short enough fit the expression-bodied form, and the rest of the lesson uses it freely.

Returning Multiple Values With Tuples

Methods that need to hand back several related values used to have three bad options: pack them into a custom class, use out parameters, or use the old System.Tuple<T1, T2> type with .Item1 and .Item2 properties. None of those read well.

C# 7 introduced value tuples, a lightweight syntax for grouping a handful of values without inventing a type. They look like this in a return position:

The return type (decimal subtotal, decimal tax, decimal total) declares a tuple with three named fields. The body builds those three values and returns them with the tuple literal (subtotal, tax, total). At the call site, order.subtotal, order.tax, and order.total give back each piece by name.

The underlying type is System.ValueTuple<T1, T2, T3>, a struct. That makes value tuples cheap, they live on the stack like other structs and don't allocate on the heap. The names exist mostly for the compiler and the IDE; at runtime, the same data is also accessible through .Item1, .Item2, .Item3 if you ever need them.

Naming the Fields

Names are optional but almost always worth adding. Compare an unnamed tuple with a named one:

A call site shows the difference:

Both produce the same numbers, but the named version tells you what each one means without having to look up the method. For anything beyond a one-off pair, name the fields.

Deconstruction at the Call Site

Reading order.subtotal works, but it's still wrapping the values in a tuple variable. Often you'd rather pull them straight into separate local variables. That's what deconstruction does:

Three useful forms:

  • var (a, b, c) = ... introduces three new locals with inferred types.
  • (int a, int b, int c) = ... introduces three new locals with explicit types.
  • (a, b, c) = ... (no var, no types) assigns into variables that already exist.

You can't mix the first two on the same line. Either every name gets var-style inference (one var for the whole tuple, not one per name) or every name carries an explicit type.

The variable names on the left don't have to match the tuple's field names. The deconstruction is positional, the first field of the tuple goes into the first variable on the left regardless of what either is called. That's error-prone and worth flagging:

What's wrong with this code?

The output is Subtotal: 8, Tax: 100, which is almost certainly not what the author intended. Deconstruction is by position, so tax on the left receives the first tuple field (the actual subtotal) and subtotal receives the second (the actual tax). The fix is to either keep the names in the same order as the tuple definition (var (subtotal, tax) = ...) or use member access instead (var order = ComputeOrder(); decimal subtotal = order.subtotal;).

Discards in Deconstruction

When you only care about some of the tuple's fields, use the underscore _ (called a "discard") to ignore the rest:

A discard isn't a variable. It's a signal to the compiler that this slot exists but isn't worth a name. You can't read from _ after the deconstruction; the underscore doesn't even exist as a symbol. Using it makes the intent clearer than naming a variable unused or dummy and adds nothing to memory because the discarded value isn't stored anywhere.

You can have as many discards in a single deconstruction as you like, even all of them, though all-discards is a sign the method call has no useful effect.

A Diagram of a Method's Shape

A method takes parameters in, runs some logic, and returns one value out. With tuples, that "one value" can carry several pieces inside it, but it's still a single return.

The blue nodes on the left are the parameters flowing in. The orange node is the method itself. The green node is the single returned tuple. The teal nodes are the named fields the caller pulls out of that tuple via deconstruction or property access. There's still exactly one return value; the tuple is just the wrapper around the three pieces.

Tuples vs out Parameters

The _ref, out & in Parameters_ lesson covered out parameters, which let a method assign through a caller-supplied location. Before tuples existed, that was the standard way to return more than one value. Both still work; the question is which to use when.

A side-by-side rewrite of the order calculation makes the differences clear:

Both produce identical results. The differences are in the ergonomics and the situations each one fits best:

AspectTuple returnout parameters
Caller syntaxOne expression, can be used inlinePre-declared (or inline out var) variables
Method signatureReturn type carries the shapeParameter list grows with each output
Use in chained callsYes, can be passed to another methodNo, can't be chained directly
LINQ / lambda friendlinessWorks naturallyAwkward, lambdas can't have out parameters easily
Composition with asyncWorks with Task<(...)> returnsout is not allowed on async methods
Discard support_ works directlyout _ works but is more verbose
Best fit2-4 related values that always come togetherTry* patterns where one value is the result and others are diagnostics

The most common idiomatic use of out today is the Try* pattern. The method returns a bool ("did it succeed?") and writes the actual value through an out parameter:

The standard library is full of this pattern: int.TryParse, Dictionary.TryGetValue, Queue.TryDequeue. It works well because the success bool and the value have different lifetimes, the value is only valid when the bool is true, and the call site naturally checks one before using the other.

Outside Try*, prefer tuples. They compose better with LINQ, async, and pattern matching, they don't pollute the parameter list, and they make the "what comes out" part of the contract live in the return type where it reads first.

A clean tuple-based validation example:

The return type spells out exactly what the caller will get back: a bool flag and a string explanation. That's much friendlier than a bool ValidateOrder(..., out string reason) signature, especially when the method has to be passed around as a value, used in LINQ, or returned from a higher-order method.

Value tuples are structs. Returning one is essentially free, like returning any other struct. They're allocated on the stack frame of the caller (or boxed only when stored in an object or non-generic collection).

Returning Collections and Custom Types

Methods often return collections of items or custom-shaped data. Those return types work exactly like primitives from the method's point of view, the only thing that changes is what callers can do with the result.

Returning an Array

Returning a List<T>

The .Sum() call uses LINQ. The Arrays section and the Collections section in later chapters go into return-type choices for collections (returning IEnumerable<T> vs IReadOnlyList<T> vs List<T>, deferred vs immediate execution). For this lesson, the point is just that the return-type machinery doesn't care: a List<decimal> returns the same way as a decimal.

Returning a String

Returning a Custom Type

For shapes that are reused across many methods, a class or struct beats a tuple. Tuples are great for one-off returns, but giving the shape a name (Order, CartSummary, ValidationResult) is clearer once it shows up in five or six places.

A short note on a topic that comes up often: C# 9 introduced records, which are a compact syntax for declaring small immutable data types with value-based equality. They're a natural fit for return shapes that need names. The tuple-vs-record decision comes down to whether the shape is one-off (tuple) or reusable and worth naming (record or class). Either way, the return-keyword mechanics are identical: build the value, hand it back, the caller uses it.