C# Static Constructor: Syntax and Runtime Behavior
Learn C# static constructor syntax, execution timing, exception behavior, thread-safety considerations, and practical initialization patterns.
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 definition | beforefieldinit? | Static initialization runs when? |
|---|---|---|
static int X = 1; (no static ctor) | Yes | Anytime before first access, may be earlier |
| Has explicit static constructor | No | Immediately 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 type initializer is not run again, 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 synchronizes concurrent access. If two threads access the type simultaneously, one thread performs the initialization and the other waits until it completes. Static constructors are therefore safe for initializing shared static state without additional locking.
The synchronization can still cause problems when initialization crosses type boundaries. If a static constructor calls into another type whose initialization waits on the current type, the behavior depends on timing and runtime version: it can deadlock, or it can allow reentrant access before initialization is complete. Avoid static-constructor chains that can cycle; do not rely on the runtime to detect them.
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. It is fragile and can fail in different ways depending on how the types are first accessed and on the runtime. Keep type initializers focused on the type's own state and avoid calls that might reenter another type initializer.
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 also removes the beforefieldinit flag. That removal gives more deterministic initialization timing, but it is not needed for a simple field assignment. Choose a static constructor when initialization logic spans multiple statements or includes conditional logic that a field initializer cannot express.
Using a Static Constructor in a Singleton
A common pattern that relies on static constructors is the singleton pattern. A static field and a static constructor can provide 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.
A normal Lazy<T> still caches exceptions from a failed factory. If you need retry behavior, pair it with a reset method that swaps in a new Lazy<T> instance, as shown later. A static constructor cannot provide that flexibility, because once a type initializer fails, the type is permanently unusable.
Compatibility and Production Considerations
Static constructors are a stable language feature across .NET versions, and the core behavior is consistent in .NET Framework, .NET Core, and .NET 5+. If you emit dynamic types with AssemblyBuilder and IL, the same type-initialization rules apply: the runtime invokes the emitted static constructor according to the metadata and the access pattern.
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 = System.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
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.
Primary constructors capture parameters for instance creation only, so they do not replace static initializers. When refactoring a class to use primary constructors, keep any static initialization logic in a static constructor or static field initializer. Moving static field logic into the primary constructor would cause it to run for every instance rather than once, which would break 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 released 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 a resetable Lazy<T> 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 lets you replace the failed lazy instance so the next Instance access retries initialization. The default Lazy<T> 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.