Repository navigation
Description
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
Valuere-invoke the factory, allowing transient failures to recover. - Once the factory succeeds, the value is cached normally (
IsValueCreatedbecomestrue).
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 resultAlternative Designs
- New
LazyThreadSafetyModeenum 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. LazyOptionsflags 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 theLazy<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
danmoseley commentedon Sep 24, 2018
@stephentoub
stephentoub commentedon Sep 24, 2018
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, andExecutionAndPublication), should be fine.danmoseley commentedon Sep 24, 2018
Shall we flip this to ready-for-review and we can discuss naming there?
GSPP commentedon Sep 26, 2018
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
ExecutionAndPublicationmode 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.
stephentoub commentedon Sep 26, 2018
Thinking about this more, I'd prefer a slightly different design. There's nothing in the
LazyThreadSafetyModeper 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 bothNoneandExecutionAndPublicationto not cache the exception (today they do), and forPublicationOnlyto 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 inLazyThreadSafetyMode, e.g.mariusGundersen commentedon Sep 26, 2018
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.
stephentoub commentedon Sep 26, 2018
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.
mariusGundersen commentedon Sep 26, 2018
I see what you mean. It's a bit weird that two of the
LazyThreadSafetyModeenum 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 theLazyThreadSafetyModeenum is strangely named andLazyis 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 whattruemeans in a list of arguments, it's easier to understand whatExceptionStrategy.Retrymeans 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 😄stephentoub commentedon Sep 26, 2018
There wouldn't be a default value.
Thankfully we have named arguments now, e.g.
mariusGundersen commentedon Sep 26, 2018
'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.
mariusGundersen commentedon Sep 26, 2018
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 👍
svick commentedon Sep 27, 2018
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 theTaskwill be produced successfully, even if the request fails, so the failingTaskwould still be cached byLazy.GSPP commentedon Sep 30, 2018
Async support for
Lazywould be very useful. I have added a ticket for that: Add async support to System.Lazy dotnet/corefx#32552.mariusGundersen commentedon Feb 11, 2019
@stephentoub, what is the status of this?
stephentoub commentedon Feb 11, 2019
Nothing has changed. It's still marked as api-suggestion, and the design was still being debated.
15 remaining items
Add Lazy<T> constructor with cacheExceptions parameter
Add Lazy<T> constructor with cacheExceptions parameter
kevincathcart-cas commentedon Aug 17, 2026
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 ofLazy<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.kevincathcart-cas commentedon Aug 17, 2026
The description seems to say that
cacheExceptionsbeing 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? 1I 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 takesLazyThreadSafetyModeand a factory has caching varying by the provided mode, and this new constructor will have caching vary only by the new parameter.Lazy(T value)Lazy(Func<T> valueFactory)Lazy(bool isThreadSafe)Lazy(LazyThreadSafetyMode mode)Lazy(Func<T> valueFactory, bool isThreadSafe)Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode)modeisPublicationOnlyLazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool cacheExceptions)cacheExceptionsFootnotes
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. ↩bartonjs commentedon Oct 6, 2026
Video
Manish-1kumar commentedon Nov 5, 2026
@stephentoub I see there's a prototype on your
api-proposal/lazy-no-exception-cachingbranch. Are you planning to open a PR from it? If not, I'd be happy to take this on and implement the approved API (cacheExceptionsconstructor,IsValueFaulted, and theLazy<T, TMetadata>overload).stephentoub commentedon Nov 5, 2026
I'm not. Thanks.
Manish-1kumar commentedon Nov 5, 2026
Thanks! I'll take it then. One semantic question before I open a PR: for
LazyThreadSafetyMode.PublicationOnlywithcacheExceptions: 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: falseretries the factory on the next access, andtruematches today's behavior.bartonjs commentedon Nov 5, 2026
@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.
Manish-1kumar commentedon Nov 5, 2026
Thanks @bartonjs, that makes sense. I've opened #137044:
cacheExceptionsis authoritative in every mode (including caching forPublicationOnlywhen it'strue), and the existing(Func<T>, LazyThreadSafetyMode)constructor passes the compatible value for each mode.