Back to Blog
C#

C# Static Constructor: Syntax and Runtime Behavior

c# static constructor: Learn C# static constructor syntax, when the runtime executes type initializers, and how static constructor exceptions and timing affect product...

static constructortype initialization.NET runtimeC# programmingbeforefieldinitsingleton pattern
Diagram of a C# static constructor triggering before first type access, showing a one-time lock and initialization flow.

A C# static constructor, also known as a type initializer, runs once before the first instance of a type is created or any static member is referenced. Its exact execution timing is not always obvious, especially when static fields have initializers or when the JIT compiler applies optimizations. This article explains the syntax, runtime guarantees, and practical implications of static constructors, including exception behavior and thread safety.

Declaring a Static Constructor

A static constructor is declared by using the static keyword without any access modifiers. It cannot take parameters, cannot return a value, and cannot be called directly. The syntax is straightforward:

public class DatabaseConnection { private static readonly string ConnectionString; static DatabaseConnection() { ConnectionString = LoadConnectionString(); Console.WriteLine("Static constructor executed."); } private static string LoadConnectionString() { // Read configuration from a file or environment return "Server=.;Database=App;Integrated Security=true"; } }

The static constructor assigns the static field. It runs once per type per application domain, and the runtime guarantees that it runs on a single thread even if multiple threads try to access the type simultaneously.

Static constructors are often used to initialize static fields that require more logic than a simple assignment, such as reading configuration, setting up a logging infrastructure, or loading lookup tables.

When the Runtime Executes a Static Constructor

The .NET runtime triggers a static constructor before the first instance of the type is created or before the first static member is accessed. This includes static methods, static properties, and static fields. For example:

public class Configuration { public static int Timeout = LoadTimeout(); static Configuration() { Console.WriteLine("Static constructor for Configuration."); } private static int LoadTimeout() { return 30; } } class Program { static void Main() { Console.WriteLine(Configuration.Timeout); } }

Here the static field initializer and the static constructor order matters. The runtime executes field initializers in textual order before the body of the static constructor. The output will be:

Static constructor for Configuration.
30

The static constructor runs before Timeout is read, because reading a static field is a trigger.

The beforefieldinit Optimization

A subtle but critical behavior arises from the presence of a static constructor. In C#, a type with a static constructor is not marked with beforefieldinit in the metadata. Types without an explicit static constructor may be marked with beforefieldinit, allowing the runtime to execute static field initializers at an earlier, less predictable time.

When a type has no static constructor, the runtime may initialize static fields just before first use, but it is allowed to do so earlier, even on a different thread. This can affect timing of side effects.

Type definitionbeforefieldinit?Static constructor executes when?
static int X = 1; (no static ctor)YesAnytime before first access, may be earlier
Has explicit static constructorNoImmediately before first access

Because a static constructor forces the runtime to guarantee that the initializer runs exactly once before first access, you can rely on the timing of the side effects. Without it, observable side effects may occur earlier than expected, which is rarely a problem but becomes significant if you log initialization or perform expensive work.

Static Constructor Exceptions

When a static constructor throws an exception, the runtime wraps it in a TypeInitializationException and records it as the inner exception. Any subsequent attempt to access the type will throw the same TypeInitializationException without re-running the static constructor. The type remains unusable for the lifetime of the application domain.

public class UnstableService { static UnstableService() { throw new InvalidOperationException("Configuration missing."); } public static string Status => "ready"; } class Program { static void Main() { try { var status = UnstableService.Status; } catch (TypeInitializationException ex) { Console.WriteLine(ex.InnerException); } // This throws again, even though the first attempt failed. try { var status = UnstableService.Status; } catch (TypeInitializationException) { Console.WriteLine("Still failing."); } } }

The exception is not re-executed, but the same exception is thrown again. This behavior prevents partial initialization and makes it impossible to retry initialization later.

Thread Safety and Deadlock Considerations

The runtime guarantees that a static constructor is executed only once per type, and it uses a lock to synchronize concurrent access. If two threads access the type simultaneously, one thread performs the initialization and the other waits. This makes static constructors safe for initializing shared static state without additional locking.

However, this locking can lead to deadlock if the static constructor calls back into another type whose static constructor waits on current type initialization. For example:

public class A { static A() { Console.WriteLine("A initializing"); var unused = B.Value; } public static string Value => "A"; } public class B { static B() { Console.WriteLine("B initializing"); var unused = A.Value; } public static string Value => "B"; }

This forms a cycle. The runtime will detect the cycle and throw an exception rather than deadlock, but the exact exception type and behavior is implementation-dependent. In practice, you should avoid static constructor chains that can cycle.

Static Constructor vs Static Field Initializer

Many developers assume that a static constructor is necessary to initialize static fields. In C#, static field initializers are sufficient for simple assignments, and they run before the static constructor body. If you have multiple static fields, they are initialized in the order they appear.

Sometimes you need a static constructor because you need to perform logic that cannot be expressed as a field initializer, such as conditional assignment, calling multiple methods, or setting up other types' static state. For a single field initializer, a static field initializer is more concise:

public class Settings { public static readonly int MaxRetries = LoadMaxRetries(); }

Using a static constructor for simple assignments adds unnecessary boilerplate and may accidentally introduce the beforefieldinit difference, which is actually not a problem if you have a static constructor because the absence of beforefieldinit gives you a deterministic timing. But the extra code can obscure the intent.

Choose a static constructor when initialization logic spans multiple steps or requires exception handling that you want to propagate as TypeInitializationException.

Using a Static Constructor in a Singleton

A common pattern that relies on static constructors is the singleton pattern. The .NET implementation uses a static field and a static constructor to guarantee lazy thread-safe initialization.

public sealed class Logger { private static readonly Logger _instance = new Logger(); static Logger() { } private Logger() { // Set up resources } public static Logger Instance => _instance; }

The empty static constructor ensures the type is not marked beforefieldinit, so the field initializer runs exactly when the Instance property is accessed. Without the static constructor, the runtime could initialize the instance earlier, which is usually fine but may be undesirable if the logger creation has side effects that must occur at a known point.

Static Constructors and Performance

The cost of a static constructor is generally negligible for most applications. The runtime calls the method once and then uses a type initialization flag to skip it on subsequent accesses. However, the first access to a type with a static constructor may incur a small synchronization overhead, especially under high concurrency.

If static initialization does heavy work—such as reading a large file, connecting to a remote service, or scanning the file system—that work will block the first thread that triggers the type. Subsequent accesses are fast. If you need to avoid blocking at startup, consider using Lazy<T> with LazyThreadSafetyMode.ExecutionAndPublication for more control over initialization and exception caching.

The Lazy<T> approach also allows you to catch initialization exceptions and retry later, which a static constructor cannot do, because once a type initializer fails, the type is permanently unusable.

Compatibility and Production Considerations

Static constructors are a stable language feature across all .NET versions, and they behave consistently in .NET Framework, .NET Core, and .NET 5+. The main compatibility risk is the interaction with reflection and dynamic code generation. When you use Assembly.Load or reflection to invoke a static method, the static constructor runs as expected. But if you create a type dynamically using RuntimeHelpers or IL emit, you must follow the same metadata rules for the static constructor to be recognized.

In production, a failed static constructor often surfaces as a TypeInitializationException right after a deployment, indicating a misconfiguration. Because the exception is not retried, a common mitigation is to avoid doing error-prone work in a static constructor. Instead, perform configuration validation at startup in a normal method that can be called again, or use a Lazy<T> that can be reset.

For hot reload scenarios in development, a static constructor that captures environment state may not reflect changes after application startup. This is usually acceptable because static state persists for the process lifetime.

public sealed class ServiceLocator { private static readonly IServiceProvider _provider; static ServiceLocator() { _provider = BuildProvider(); } private static IServiceProvider BuildProvider() { // Use a dependency injection container return new ServiceCollection() .AddSingleton<ILogger, ConsoleLogger>() .BuildServiceProvider(); } public static T Get<T>() where T : class => _provider.GetService<T>() ?? throw new InvalidOperationException("Service not registered."); }

This pattern centralizes service resolution. The static constructor runs once, so you avoid reconstructing the provider on every call.

Static Constructors in Generic Types

For generic types, a static constructor runs once per constructed type. For example, Factory<int> and Factory<string> each have their own static initializer.

public class Factory<T> { static Factory() { Console.WriteLine($"Initializing Factory<{typeof(T).Name}>"); } public static T Create() => default; } class Program { static void Main() { var intItem = Factory<int>.Create(); var stringItem = Factory<string>.Create(); } }

The output will show two initialization messages, one per type parameter. This is useful for per-type caches, but it means that a generic type with many type instantiations repeats the initialization overhead for each.

A common use is a generic repository where each entity type has its own static cache:

public class Cache<TKey, TValue> { private static readonly Dictionary<TKey, TValue> Items = new(); static Cache() { // Perform validation or preload for each specific generic type. } public static TValue Get(TKey key) => Items[key]; }

Each combination of type arguments gets its own static data, which isolates them from each other.

Static Constructor and the init Accessor

If your static property uses the init accessor, you cannot assign it in a static constructor because init only allows assignment during object initialization. Use a static field instead, or make the property read-only with a private set. The static constructor can set a public static string Name { get; private set; } but not public static string Name { get; init; }. The latter will produce a compile-time error. This is a boundary that developers sometimes run into when mixing modern C# features with static initialization.

Suppose you want a static property that is immutable after assignment:

public class AppSettings { public static string Environment { get; private set; } = "Production"; static AppSettings() { Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; } }

This works because the setter is private, not init. The static constructor acts as the single point of assignment.

Advanced: Static Constructor in a Nested Type

The runtime treats nested types as separate types for static initialization. A nested type with a static constructor is initialized only when that nested type is first accessed, not when the containing type is accessed. This allows you to defer work until the nested type is needed.

public class Outer { public static class Config { static Config() { Console.WriteLine("Config static ctor"); } public static int Port => 8080; } } class Program { static void Main() { Console.WriteLine("Before touching Config"); Console.WriteLine(Outer.Config.Port); } }

The "Config static ctor" line appears just before accessing Port, not at program start.

Use nested types to isolate initialization scopes. This can improve startup time if only some features are used on a given execution path.

Static Constructor vs Primary Constructor (coming in C# 12)

C# 12 introduced primary constructors for non-record classes, but they apply to instance constructors, not static constructors. You cannot use primary constructor syntax to replace a static constructor. Static constructor syntax remains the only way to write initializer logic for the type itself.

There is no planned feature that replaces the static constructor; it is a distinct concept from instance constructors and primary constructors.

When you see a class with a primary constructor and a static field, the static field initializer or static constructor still controls the type-level state.

This distinction matters when you are refactoring a class to use primary constructors: you must keep any static initialization logic in a static constructor or a static field initializer, because primary constructors capture parameters for instance creation only.

A typical mistake is to move static field logic into the primary constructor, which causes it to run for every instance rather than once. That would break any caching or one-time setup assumptions.

Static Constructor in .NET MAUI and Blazor

In UI frameworks like .NET MAUI and Blazor, static constructors are used to set up platform-specific resources or DI containers. Because a static constructor runs once per process, it is appropriate for initializing global services that do not change with the UI lifetime.

However, in Blazor WebAssembly, the process lifetime is the browser tab. If your static constructor holds onto unmanaged resources or large arrays, they will be collected when the page closes. Also, Blazor's hot reload may not reset static state when you modify a static constructor; you may need to reload the page.

In MAUI, a static constructor could be used to initialize a database connection string, but be aware that the static constructor runs on the thread that first accesses the type. If that thread is a UI thread and the initialization is slow, it will cause a visible freeze. In that case, prefer asynchronous initialization in an instance method or a Lazy<Task<T>>.

Final Practical Pattern: Lazy Retriable Initialization

If you need to retry initialization after a failure, a static constructor is unsuitable. The standard alternative is to use Lazy<T> with a resetable wrapper:

public class RetryableInitializer { private static Lazy<ServiceClient> _lazy = new(Init); public static ServiceClient Instance => _lazy.Value; private static ServiceClient Init() { return new ServiceClient("config"); } public static void Reset() { Interlocked.Exchange(ref _lazy, new Lazy<ServiceClient>(Init)); } }

Here, Reset allows you to retry if _lazy.Value throws. The Lazy<T> default ExecutionAndPublication mode ensures thread safety. This pattern gives you the same lazy one-time initialization but avoids the permanent failure behavior of a static constructor.

This is a practical compromise when you need retry logic after a configuration change. Keep static constructors for immutable, critical infrastructure that must never fail silently.

c# static constructor: Practical Usage and Code Examples | RYUSLOG DEV