Skip to content

Introduce a fourth type of LazyThreadSafetyMode: ThreadSafeValueOnly #27421

Description

@mariusGundersen

Note

Edited by @tannergooding to match #27421 (comment)

Note

This proposal was drafted with assistance from GitHub Copilot based on the discussion in this issue.

Background and motivation

Lazy<T> with ExecutionAndPublication (or None) mode caches exceptions thrown by the value factory. Once the factory throws, every subsequent access to Value rethrows the same cached exception — the Lazy<T> is permanently faulted.

This is a problem for scenarios where the factory performs an operation that can fail transiently (network calls, database queries, file I/O) but is expected to succeed on retry. Today, if the factory fails, the only recovery is to discard the entire Lazy<T> instance and create a new one — which requires external synchronization and defeats the purpose of using Lazy<T>.

PublicationOnly mode does not cache exceptions, but it allows multiple threads to invoke the factory concurrently, causing "stampede" issues when the goal is to serialize access to a shared resource.

This has been a long-standing pain point with 34+ upvotes on this issue and a popular Stack Overflow question. Users are forced to write their own Lazy<T> replacements (see workaround in this thread) or use LazyInitializer.EnsureInitialized with manual state management.

Exception caching is orthogonal to thread-safety mode. Per this comment from @stephentoub, a separate bool parameter is the right way to expose this rather than adding a new LazyThreadSafetyMode enum value.

API Proposal

namespace System;

public partial class Lazy<T>
{
    // EXISTING
    // public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);

    public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool cacheExceptions);
}

When cacheExceptions is false:

  • If the value factory throws, the exception is not cached. The factory reference is preserved.
  • Subsequent accesses to Value re-invoke the factory, allowing transient failures to recover.
  • Once the factory succeeds, the value is cached normally (IsValueCreated becomes true).

When cacheExceptions is true, behavior is identical to existing constructors.

Prototype: b2287e7 (branch api-proposal/lazy-no-exception-caching)

API Usage

// Cache a network-fetched configuration that may fail transiently
var config = new Lazy<AppConfig>(
    () => httpClient.GetFromJsonAsync<AppConfig>(configUrl).GetAwaiter().GetResult(),
    LazyThreadSafetyMode.ExecutionAndPublication,
    cacheExceptions: false);

// First access: may throw if network is down
// Second access: retries the factory instead of rethrowing the cached exception
try
{
    var value = config.Value;
}
catch (HttpRequestException)
{
    // Transient failure — next access will retry
}

// Later access succeeds once network recovers
var value = config.Value; // calls factory again, succeeds, caches the result

Alternative Designs

  • New LazyThreadSafetyMode enum value (original proposal): Rejected because exception caching is orthogonal to thread-safety mode. A new enum value conflates two independent concerns and doesn't allow controlling exception caching for all modes.
  • LazyOptions flags enum: Suggested as a more extensible alternative. Adds more API surface for a single boolean concept. Could be revisited if additional options are needed in the future.
  • Existing workaround using LazyInitializer.EnsureInitialized: Works but requires manual management of three separate fields (ref T, ref bool, ref object) and loses the Lazy<T> encapsulation.

Risks

The new constructor adds one bool field (_cacheExceptions) to every Lazy<T> instance. This increases instance size slightly (1 byte + padding). An alternative implementation could encode this in the existing LazyHelper/LazyState state machine to avoid per-instance overhead for the common case.

No binary or source breaking changes — the new constructor has a unique signature (Func<T>, LazyThreadSafetyMode, bool) that does not conflict with any existing constructor.

Activity

  1. danmoseley commented on Sep 24, 2018

    @danmoseley
    Contributor
  2. stephentoub commented on Sep 24, 2018

    @stephentoub
    Member

    stephentoub

    Seems reasonable functionally, albeit with a better name 😄. Maybe ExecutionAndPublicationValueOnly, ValueExecutionAndPublication, ExecutionAndPublicationSuccess, or something along those lines. As long as it's consistent with the other current names (None, PublicationOnly, and ExecutionAndPublication), should be fine.

  3. danmoseley commented on Sep 24, 2018

    @danmoseley
    Contributor

    Shall we flip this to ready-for-review and we can discuss naming there?

  4. GSPP commented on Sep 26, 2018

    @GSPP

    This is very important to have. If a lazy instance is used as a cache in a web application and it's execution fails then the web app is permanently down. Many things worth caching can fail transiently (any database or web service call). The ExecutionAndPublication mode is fundamentally unsafe in many scenarios.

    The weaker modes are not very nice to use in caching scenarios because they can cause cache stampeding due to multiple executions.

  5. stephentoub commented on Sep 26, 2018

    @stephentoub
    Member

    Thinking about this more, I'd prefer a slightly different design. There's nothing in the LazyThreadSafetyMode per se that's about whether exceptions are cached or not; although the XML docs go into details about resulting behavior, the values are really just about the thread-safety nature of the operation. As such, whether exceptions are cached or not could actually apply to any of the existing values, and it would be better if that option were thusly applicable to all of them. This would give the ability for both None and ExecutionAndPublication to not cache the exception (today they do), and for PublicationOnly to cache the exception (today it doesn't). So, I'd prefer to see this exposed as one or more new ctors rather than as a new value in LazyThreadSafetyMode, e.g.

    public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool storeExceptions);
  6. mariusGundersen commented on Sep 26, 2018

    @mariusGundersen
    ContributorAuthor

    There's two reasons I added a new enum value rather than add a boolean argument:

    First of I'm of the opinion that adding bools as arguments is bad for readability of the code, and that it is better to use enums or have multiple methods. This is well documented and is a pattern I've seen other places in C#, for example in Index Of, which could have taken a boolean for case invariance but instead uses an enum with several values.

    And second is that I don't think features should be added unless they are needed. Yes, the other options could use a flag for toggling caching of exceptions too, but is there a real demand for that? It would just make the already complex Lazy even more complex, maybe not much in code but definitely in understanding all the possible combinations and which one is the correct one for the situation at hand.

  7. stephentoub commented on Sep 26, 2018

    @stephentoub
    Member

    I understand your reasons. But whether the exception is cached or not has little to do with the thread safety mode of the object, which is what that enum is about. From my perspective, adding an unrelated and orthogonal value to an enum is more confusing and more complex than adding it as a separate argument. In the IndexOf example, case is a part of how the string comparison happens, so it makes sense as part of something name "StringComparison"; that's not the case here.

  8. mariusGundersen commented on Sep 26, 2018

    @mariusGundersen
    ContributorAuthor

    I see what you mean. It's a bit weird that two of the LazyThreadSafetyMode enum values cache the exception but one of them don't, so the bool final argument will have a strange definition for its default value. IMO the LazyThreadSafetyMode enum is strangely named and Lazy is already a bit too complicated to use, and probably should have been multiple different classes, but that's too late to fix, so it's not really possible to enhance it without making it even more complex. I am still not fond of bools as the final argument, and would prefer another separate enum for readability of code. It's hard to know what true means in a list of arguments, it's easier to understand what ExceptionStrategy.Retry means and what the called method will do. But, I'm more interested in having this feature than to get tied up in api design discussions, so I can live with the bool parameter 😄

  9. stephentoub commented on Sep 26, 2018

    @stephentoub
    Member

    will have a strange definition for its default value

    There wouldn't be a default value.

    It's hard to know what true means in a list of arguments

    Thankfully we have named arguments now, e.g.

    new Lazy<T>(..., storeExceptions: true);
  10. mariusGundersen commented on Sep 26, 2018

    @mariusGundersen
    ContributorAuthor

    'Default value' as in one of the overloads that don't take the bool argument. Most of the time when there is an overload without a bool argument it's clear how it will behave, as if there was a default value, but in this case it might be more difficult to document clearly.

  11. mariusGundersen commented on Sep 26, 2018

    @mariusGundersen
    ContributorAuthor

    But I'm not objecting to your suggestion, so if there is a vote or something now (I've never proposed something here before) it has my vote 👍

  12. svick commented on Sep 27, 2018

    @svick
    Member

    If the example use case is caching the result of a web request, doesn't that mean adding this would promote waiting synchronously for IO operations?

    And I think the pattern of using Lazy<Task<T>> for this purpose, as suggested by @stephentoub in an article from 2011, wouldn't work well here, because the Task will be produced successfully, even if the request fails, so the failing Task would still be cached by Lazy.

  13. GSPP commented on Sep 30, 2018

    @GSPP

    Async support for Lazy would be very useful. I have added a ticket for that: Add async support to System.Lazy dotnet/corefx#32552.

  14. mariusGundersen commented on Feb 11, 2019

    @mariusGundersen
    ContributorAuthor

    @stephentoub, what is the status of this?

  15. stephentoub commented on Feb 11, 2019

    @stephentoub
    Member

    Nothing has changed. It's still marked as api-suggestion, and the design was still being debated.

  16. 15 remaining items

  17. added
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    and removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Jul 11, 2026
  18. kevincathcart-cas commented on Aug 17, 2026

    @kevincathcart-cas

    Should a similar constructor overload be added to Lazy<T, TMetadata>? I know it exists primarily for MEF, but it is a general-purpose mechanism, and it does currently have equivalents for all the constructor overloads of Lazy<T>, except for the one that takes a preconstructed T. I can't see any reason not to add it, except for the tiny increase in assembly size.

  19. kevincathcart-cas commented on Aug 17, 2026

    @kevincathcart-cas

    The description seems to say that cacheExceptions being true is the default behavior. I thought the idea from earlier in this thread was that this argument would do what it says on the tin, and control exception caching for all modes, even the currently never cached publication-only? 1

    I would also suggest that instead of documenting any of the caching behavior on LazyThreadSafetyMode (which currently documents this), it should simply indicate that various values of this flag may impact exception caching depending on the constructor used, and then point them to constructor docs for details, and have those document caching behavior, as the caching behavior differs significantly depending on which constructor overload is used. Even without this change the caching behavior varied by constructor: Constructors that don't take a factory never cache, the existing constructor that takes LazyThreadSafetyMode and a factory has caching varying by the provided mode, and this new constructor will have caching vary only by the new parameter.

    constructor Behavior
    Lazy(T value) Never caches, as cannot throw
    Lazy(Func<T> valueFactory) Always caches
    Lazy(bool isThreadSafe) Never caches
    Lazy(LazyThreadSafetyMode mode) Never caches
    Lazy(Func<T> valueFactory, bool isThreadSafe) Always caches
    Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode) Caches except when mode is PublicationOnly
    new Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool cacheExceptions) Caching is controlled by cacheExceptions

    Footnotes

    1. From an implementation perspective: It looks to be possible to implement publication only with cached exceptions, without negatively impacting perf for the existing un-cached publication only, by having separate LazyStates for publication only with caching and using the helper for those states. ↩

  20. bartonjs commented on Oct 6, 2026

    @bartonjs
    Member

    Video

    • The default of the exception caching depends on the mode already, so there's not a bias for a default-true or default-false position.
    • The documentation refers to "exception caching", so "cacheExceptions" seems good.
    • Exposing the value-falted state seems like related goodness.
    • And we remembered it has an arity-2 derived type.
    namespace System;
    
    public partial class Lazy<T>
    {
        public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool cacheExceptions);
        public bool IsValueFaulted { get; }
    }
    
    public partial class Lazy<T, TMetadata> : Lazy<T>
    {
        public Lazy(Func<T> valueFactory, TMetadata metadata, LazyThreadSafetyMode mode, bool cacheExceptions);
    }
  21. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Oct 6, 2026
  22. Manish-1kumar commented on Nov 5, 2026

    @Manish-1kumar
    Contributor

    @stephentoub I see there's a prototype on your api-proposal/lazy-no-exception-caching branch. Are you planning to open a PR from it? If not, I'd be happy to take this on and implement the approved API (cacheExceptions constructor, IsValueFaulted, and the Lazy<T, TMetadata> overload).

  23. stephentoub commented on Nov 5, 2026

    @stephentoub
    Member

    Are you planning to open a PR from it?

    I'm not. Thanks.

  24. Manish-1kumar commented on Nov 5, 2026

    @Manish-1kumar
    Contributor

    Thanks! I'll take it then. One semantic question before I open a PR: for LazyThreadSafetyMode.PublicationOnly with cacheExceptions: true, should exceptions now be cached (so the flag controls caching in every mode, as @kevincathcart-cas suggested), or should it keep today's non-caching behavior ("identical to existing constructors", per the proposal text)? The other modes are clear: cacheExceptions: false retries the factory on the next access, and true matches today's behavior.

  25. bartonjs commented on Nov 5, 2026

    @bartonjs
    Member

    @Manish-1kumar When the parameter is specified, it is authoritative. The overload that doesn't take the parameter will call the one that does with the correctly compatible value for each mode.

  26. Manish-1kumar commented on Nov 5, 2026

    @Manish-1kumar
    Contributor

    Thanks @bartonjs, that makes sense. I've opened #137044: cacheExceptions is authoritative in every mode (including caching for PublicationOnly when it's true), and the existing (Func<T>, LazyThreadSafetyMode) constructor passes the compatible value for each mode.

  27. added
    in-prThere is an active PR which will close this issue when it is merged
    on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Runtimein-prThere is an active PR which will close this issue when it is merged

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions