C# Optional Parameters

Optional parameters let a C# method provide default values for arguments the caller leaves out. They matter because they keep common calls short while still allowing callers to customize less common details.

Used well, optional parameters make APIs easier to read. Used carelessly, they can hide meaning, create confusing overloads, or cause versioning surprises when a default value changes later.

Overview: How Optional Parameters Work

An optional parameter is a method parameter with a default value in its declaration. For example, static void PrintMessage(string text, bool uppercase = false) has one required parameter and one optional parameter. A caller can write PrintMessage("hello"), and the compiler treats the omitted uppercase argument as false.

Optional parameters are a compile-time feature. The method itself still has the full parameter list. The important detail is that the compiler fills in missing arguments at each call site. After compilation, a call that omitted an optional argument behaves as though the default value had been written explicitly in the source code.

This means default values are not looked up dynamically each time the program runs. If a library changes an optional parameter default from false to true, already compiled callers may keep using the old value until they are recompiled. This is one reason public libraries often prefer overloads for defaults that might change.

Optional parameters must come after all required parameters in the same parameter list. This rule keeps positional calls understandable. If role = "Member" came before a required name, a call with one string would be unclear: is the string the role or the name?

Default values must be compile-time constants, null, default, or certain simple values that the compiler can embed. You can write defaults such as 0, false, "General", null, and DayOfWeek.Monday. You cannot use a runtime expression such as DateTime.Now, new Random(), or a method call as an optional parameter default.

Optional parameters often work together with named arguments. Named arguments let callers skip an earlier optional parameter and supply a later one by name, such as CreateReport("Sales", includeTotals: true). Without names, positional arguments must be supplied in order.

Syntax

static returnType MethodName(requiredType requiredName, optionalType optionalName = defaultValue)
{
    statements;
}

MethodName(requiredValue);
MethodName(requiredValue, customOptionalValue);
MethodName(requiredName: requiredValue, optionalName: customOptionalValue);
Part Meaning
requiredType requiredName A normal required parameter. The caller must provide an argument.
optionalType optionalName = defaultValue An optional parameter. The caller may omit it, and the compiler inserts the default value.
defaultValue A compile-time value such as a number, string literal, false, null, default, or enum value.
optionalName: A named argument. It makes the call clearer and can let you skip other optional parameters.

Examples

A Simple Optional Parameter

using System;

class Program
{
    static void Main()
    {
        PrintStatus("Build started");
        PrintStatus("Build failed", urgent: true);
    }

    static void PrintStatus(string message, bool urgent = false)
    {
        string prefix = urgent ? "URGENT" : "INFO";
        Console.WriteLine($"[{prefix}] {message}");
    }
}

Output:

[INFO] Build started
[URGENT] Build failed

The first call omits urgent, so the compiler supplies false. The second call uses a named argument to make the boolean value self-explanatory. This is better than PrintStatus("Build failed", true), where the meaning of true is less obvious.

Several Optional Parameters with Named Arguments

using System;

class Program
{
    static void Main()
    {
        CreateTicket("Cannot sign in");
        CreateTicket("Payment failed", priority: "High");
        CreateTicket("Typo on page", assignedTo: "Maya", sendEmail: false);
    }

    static void CreateTicket(string title, string priority = "Normal", string assignedTo = "Unassigned", bool sendEmail = true)
    {
        Console.WriteLine($"Title: {title}");
        Console.WriteLine($"Priority: {priority}");
        Console.WriteLine($"Assigned to: {assignedTo}");
        Console.WriteLine($"Email: {sendEmail}");
        Console.WriteLine();
    }
}

Output:

Title: Cannot sign in
Priority: Normal
Assigned to: Unassigned
Email: True

Title: Payment failed
Priority: High
Assigned to: Unassigned
Email: True

Title: Typo on page
Priority: Normal
Assigned to: Maya
Email: False

The method has one required parameter and three optional parameters. The third call skips priority while supplying assignedTo and sendEmail. Named arguments are what make that possible and readable.

Using null as a Default for Runtime Values

using System;
using System.Globalization;

class Program
{
    static void Main()
    {
        PrintTimestamp("Saved", new DateTime(2026, 7, 24, 14, 30, 0));
        PrintTimestamp("Queued");
    }

    static void PrintTimestamp(string label, DateTime? when = null)
    {
        DateTime actualTime = when ?? new DateTime(2026, 7, 24, 9, 0, 0);
        Console.WriteLine($"{label}: {actualTime.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)}");
    }
}

Output:

Saved: 2026-07-24 14:30
Queued: 2026-07-24 09:00

You cannot use DateTime.Now as an optional parameter default because it is a runtime value. A common pattern is to use null as the default, then choose the runtime value inside the method body. This example uses a fixed fallback time so the output is predictable.

Optional Parameters and Overload Resolution

using System;

class Program
{
    static void Main()
    {
        Save("notes.txt");
        Save("notes.txt", overwrite: true);
    }

    static void Save(string fileName)
    {
        Console.WriteLine("basic save: " + fileName);
    }

    static void Save(string fileName, bool overwrite = false)
    {
        Console.WriteLine("overwrite save: " + fileName + ", " + overwrite);
    }
}

Output:

basic save: notes.txt
overwrite save: notes.txt, True

When both overloads are available, Save("notes.txt") chooses the overload with exactly one parameter. The compiler does not automatically prefer the overload with an optional parameter. This is legal, but it can surprise readers, so avoid overload sets that compete with optional defaults.

How Optional Parameters Work Step by Step

  1. The compiler reads the method declaration and records each optional parameter’s default value in metadata.
  2. At a call site, the compiler checks which arguments the caller supplied, including any named arguments.
  3. For omitted optional arguments, the compiler inserts the declared default value into the call.
  4. Overload resolution then chooses the best method based on the resulting argument list and normal conversion rules.
  5. The compiled program calls the selected method with a value for every parameter. The CLR does not run a special optional-parameter lookup for ordinary C# calls.

This explains two important behaviors. First, a method with optional parameters still receives ordinary parameter values when it runs. Second, changing a default value in a compiled library is a source compatibility change, but callers may need recompilation before they observe the new default.

Common Mistakes

Putting Optional Parameters Before Required Parameters

static void CreateUser(string role = "Member", string name)
{
    Console.WriteLine(name + ": " + role);
}

This does not compile because a required parameter cannot follow an optional parameter. Put required information first, then defaults:

using System;

class Program
{
    static void Main()
    {
        CreateUser("Ada");
        CreateUser("Grace", role: "Admin");
    }

    static void CreateUser(string name, string role = "Member")
    {
        Console.WriteLine(name + ": " + role);
    }
}

Output:

Ada: Member
Grace: Admin

Using a Runtime Expression as a Default

static void Log(string message, DateTime when = DateTime.Now)
{
    Console.WriteLine($"{when}: {message}");
}

This does not compile because DateTime.Now is evaluated at runtime. Optional parameter defaults must be values the compiler can embed. Use null or an overload instead:

using System;
using System.Globalization;

class Program
{
    static void Main()
    {
        Log("Started", new DateTime(2026, 7, 24, 8, 15, 0));
        Log("Finished");
    }

    static void Log(string message, DateTime? when = null)
    {
        DateTime actualTime = when ?? new DateTime(2026, 7, 24, 8, 45, 0);
        Console.WriteLine($"{actualTime.ToString("HH:mm", CultureInfo.InvariantCulture)} - {message}");
    }
}

Output:

08:15 - Started
08:45 - Finished

Hiding Too Much Behind Boolean Defaults

using System;

class Program
{
    static void Main()
    {
        Export("orders.csv", true, false);
    }

    static void Export(string fileName, bool includeHeader = true, bool compress = false)
    {
        Console.WriteLine($"{fileName}: header={includeHeader}, compress={compress}");
    }
}

Output:

orders.csv: header=True, compress=False

This compiles, but the call is hard to read because true and false do not explain themselves. Prefer named arguments, or use a small options type when the settings grow:

using System;

class Program
{
    static void Main()
    {
        Export("orders.csv", includeHeader: true, compress: false);
    }

    static void Export(string fileName, bool includeHeader = true, bool compress = false)
    {
        Console.WriteLine($"{fileName}: header={includeHeader}, compress={compress}");
    }
}

Output:

orders.csv: header=True, compress=False

Best Practices

  • Use optional parameters for values that truly have a stable, obvious default.
  • Keep required parameters first and optional parameters last.
  • Use named arguments when passing optional booleans, numbers, or several values of the same type.
  • Do not use optional parameters to hide important business decisions. If the caller should think about a value, make it required.
  • Use null plus logic inside the method when the default depends on runtime state.
  • Be cautious in public libraries. Changing an optional default may not affect already compiled callers until they rebuild.
  • Avoid combining many overloads with optional parameters unless each call shape remains obvious.
  • When a method grows many optional settings, consider an options class or record instead of a long parameter list.

Practice Exercises

  1. Write a FormatName method with required firstName and lastName parameters, plus an optional includeLastFirst boolean. Use a named argument when calling it with true.
  2. Create a PrintInvoice method with a required invoice number and optional currency and showPaidStamp parameters. Print three calls that use different combinations.
  3. Write a ScheduleReminder method that accepts a required message and an optional nullable DateTime. If the date is omitted, choose a default inside the method body.

Summary

  • Optional parameters let callers omit arguments when a method declares default values.
  • The compiler inserts omitted defaults at the call site; the method still receives ordinary parameter values.
  • Defaults must be compile-time values such as literals, null, default, or enum values.
  • Named arguments make optional-parameter calls clearer and let callers skip earlier optional values.
  • Optional parameters must follow required parameters.
  • For changing public APIs, many confusing settings, or runtime defaults, overloads or options objects may be a better fit.