Skip to content

Error handling helpers

Eugene edited this page Sep 24, 2024 · 41 revisions

The WIL error handling helpers library provides a family of macros and functions that are designed to handle errors in C++ and C++/CX. The goals are to:

  • Provide uniform patterns across error handling techniques: fail-fast, error-codes, exceptions, etc.
  • Handle many commonly used error patterns in Windows code (e.g. HRESULT, NTSTATUS, and BOOL)
  • Enable exception barrier patterns to allow exception-based and return-code based code to coexist
  • Generate log messages automatically as a byproduct of error handling patterns

Usage

To use WIL error handling helpers, add wil/result.h to your C++ source file:

#include <wil/result.h>

The macros referenced below are defined in wil/result_macros.h, which is also included in result.h.

Getting failures logged

By default, WIL error handling helpers only log error messages to the debugger output. If you want to log them to an external log file or to a telemetry upload service, read the topic on Error logging and observation.

Alternative return types

WIL normalizes on HRESULT as its error code type. When an error handling helpers from wil/result_macros.h return an error code, it will return a HRESULT value.

To ease integration with code where another error type is already the default, WIL provides additional macros that return other commonly used error types. Those are defined in separate headers.

Omitting the prefix, these macros have similar names and behavior as the ones detailed below.

Integrating with C++/WinRT

By default, WIL error handling helpers aren't aware of C++/WinRT's exception types. WIL defaults to fail-fast messages for exception types it doesn't know about, which is rarely what you want for C++/WinRT exceptions.

You can fix this by including wil/cppwinrt.h before including any C++/WinRT header files in your source file:

#include <wil/cppwinrt.h> // must be before the first C++ WinRT header
#include <wil/result.h>

#include <winrt/windows.foundation.h>

This will also turn on other integrations between WIL and C++/WinRT together.

Error handling techniques

WIL error handling helpers support four key error handling techniques: exceptions, return codes, fail-fast and logging (observe and ignore). The chosen technique depends upon several factors, but as a general rule, consider the following:

  1. Prefer to handle errors with exceptions when possible; this enables full use of exception-based libraries
  2. Otherwise use uniform error code propagation
  3. Always use fail-fast when your program invariants have been violated (unrecoverable)
  4. Consider using fail-fast to handle other classes of errors if your code can accommodate it.
  5. Use logging to report non-critical failures that cannot be propagated

As an example, consider a simple DoWork() function that handles its errors through the return of an HRESULT.

To handle the error in exception-based code, you would produce an exception on failure by wrapping this in the following manner:

THROW_IF_FAILED(DoWork());

A caller who itself is using HRESULT return codes for error handling should propagate the error to its caller:

RETURN_IF_FAILED(DoWork());

If a failure of this call was unrecoverable, or the caller is otherwise using fail-fast to discover bugs, then terminate the process by wrapping the call in this way:

FAIL_FAST_IF_FAILED(DoWork());

For non-critical failures, in the event code is unable to propagate an exception or error code to its caller, rather than ignoring the error, it can be logged in this way:

LOG_IF_FAILED(DoWork());

Each of these error handling techniques are handled uniformly using a common naming pattern and a shared reporting mechanism, enabling developers to carry that familiarity with them regardless of what error handling technique is being employed.

Exception based error handling

The following example is a purely exception-based function. Errors are expected to be returned through exceptions rather than through a returned HRESULT. It makes use of a library class that can throw out of memory exceptions (std::vector), calls out to functions that return HRESULTs and converts those into exceptions via the THROW_XXXX macros.

size_t ExceptionBasedFunction(IBarMaker* barMaker)
{
    Bar bar1;
    THROW_IF_FAILED(barMaker->GetBar(&bar1));
    Bar bar2;
    THROW_IF_FAILED(barMaker->GetBar(&bar2));

    std::vector<Bar> rgBars;
    rgBars.push_back(bar1);
    rgBars.push_back(bar2);

    return ComputeValueFromBars(rgBars);
}

The THROW_XXXX macros result in throwing a wil::ResultException in pure C++ and a Platform::Exception^ in C++/CX. The exception carries with it the error code and all known information (usually file and line number) about the origin of the failure.

Using exception based code in a routine that cannot throw

When dealing with exceptions, one of the most important topics is how exception and non-exception based code interact and co-exist – a necessity within the Windows codebase.

This example is equivalent to the previous purely exception-based example, but it replaces its return value with an out parameter and shifts its error contract to an HRESULT. It must never throw an exception so the same code as the previous sample is wrapped within an exception guard.

HRESULT ErrorCodeBasedFunction(IBarMaker* barMaker, _Out_ size_t* result) noexcept try
{
    *result = 0;
    Bar bar1;
    THROW_IF_FAILED(barMaker->GetBar(&bar1));
    Bar bar2;
    THROW_IF_FAILED(barMaker->GetBar(&bar2));

    std::vector<Bar> rgBars;
    rgBars.push_back(bar1);
    rgBars.push_back(bar2);

    *result = ComputeValueFromBars(rgBars);
    return S_OK;
}
CATCH_RETURN();

When mixing error handling techniques, prefer to use function-level exception guards and avoid comingling exception and error handling code within the same function.

Dealing with functions that return errors in expected cases

C++ exceptions should not be thrown in expected cases.

This means you shouldn't convert error codes that are returned from functions that return error codes in expected cases into an exception. For example, the RegOpenKeyW function returns ERROR_NOT_FOUND when the key is not present. This is a valid scenario and should not generate an exception. Instead, the error should be reported in some other manner.

Note that there is no THROW_IF_FAIL_EXPECTED as that would encourage violations of this rule. You can use the CATCH_RETURN_EXPECTED macro to accommodate existing code that violates this rule.

C++/CX considerations

One issue that's specific to C++/CX is that the error code contract for a method implementing a UWP or other Windows Runtime API is Platform::Exception^. Only std::bad_alloc is handled and automatically converted to Platform::Exception^ by the CX generated code.

The C++/CX generated wrappers normally fail fast an unknown exception type. To instead properly handle, report and propagate the errors within C++/CX code, you need to "normalize" the exception type to Platform::Exception^ when crossing from C++/CX through the WinRT ABI boundary.

WIL provides these normalization barriers that will catch and convert to Platform::Exception^:

size_t CxAbiExceptionBasedFunction(IBarMaker* barMaker) try
{
    Bar bar1;
    THROW_IF_FAILED(barMaker->GetBar(&bar1));
    Bar bar2;
    THROW_IF_FAILED(barMaker->GetBar(&bar2));

    std::vector<Bar> rgBars;
    rgBars.push_back(bar1);
    rgBars.push_back(bar2);

    return ComputeValueFromBars(rgBars);
}
CATCH_THROW_NORMALIZED();

When to avoid exceptions

Some functions should never allow an exception to escape from within. If these functions call into code or use libraries that throw they must use an exception guard to prevent an exception from leaking.

Ensure exceptions never escape the following:

  • Functions needed to provide the "Basic" safety guarantee (that errors always leave your code in a valid state):
    • Destructors
    • Resource Release / Deallocation Functions
    • Swap Functions
    • Exception class constructors / methods
    • ScopeExit lambdas
  • Functions or callbacks used in an ABI contract implemented by your code or used by your code:
    • COM interface methods
    • DLL exports
    • System Hooks or Callbacks
  • Functions that already have an existing error-handling strategy:
    • Functions that return HRESULTs
    • Functions that have already been implicitly (or explicitly) documented as noexcept

Error code based error handling

WIL's error handling helpers emulate exceptions for error code handling through the use of early returns and macros that contain the error-code based control flow. What remains is more concise, elevating the visibility of the routine's true control flow over error handling artifacts while naturally instrumenting the possible sources of failure.

HRESULT EarlyReturnPattern(int operation) noexcept
{
    RETURN_IF_FAILED(SomeFunction());
    if (SomeCondition(operation))
    {
        RETURN_IF_FAILED(AnotherFunction());
        RETURN_IF_FAILED(YetAnotherFunction());
        RETURN_IF_FAILED(OneLastFunction());
    }
    return S_OK;
}

Avoid mixing RETURN_XXXX macros into a function's existing error control flow (if (SUCCEEDED(hr)), goto statements or alternative control flow macros). Consider converting the entire function to use WIL's early return style to avoid inadvertently inserting early returns in code that may already expect non-RAII cleanup or have other side effects. When using this style, avoid direct use of HRESULTs. If inspecting an intermediate result is required, use a const HRESULT named specifically after the operation being inspected (e.g. copyResult rather than hr).

Dealing with expected errors

Unlike exceptions, callers of HRESULT returning functions may have to deal with failure error codes in expected circumstances. Though new APIs should not return error codes in expected cases, many existing APIs return expected failures. To handle this, all RETURN_XXXX macros have a corresponding RETURN_XXXX_EXPECTED version. The _EXPECTED versions will not log on receiving the expected failure type.

For example, when calling functions that may return "expected" failures:

// the error contract of this function returns the error E_NOT_SET when the property is not present
RETURN_IF_FAILED_EXPECTED(GetGenericProperty(propertySet, key, &propertyValue));

// hwnd may become invalid at any time so failure here is expected
RETURN_IF_WIN32_BOOL_FALSE_EXPECTED(::GetWindowRect(hwnd, &rc));

It is better to only treat the specific expected error code(s) as expected, allowing all others to result in logging. This is best done using this macro, listing the expected errors at the end (up to 4):

// the error contract of this function returns the error E_NOT_SET when the component is not present
RETURN_IF_FAILED_WITH_EXPECTED(m_componentManager->ShowComponent(componentId), E_NOT_SET);

// A more verbose way to do the same
const auto hr = m_componentManager->ShowComponent(componentId);
RETURN_IF_FAILED_EXPECTED(hr, FAILED(hr) && hr == E_NOT_SET);
RETURN_IF_FAILED(hr);

Note that RETURN_XXXX_EXPECTED macros do not exist for the unconditional return macros. This is because, without logging, RETURN_HR_EXPECTED(hr) is effectively the same thing as return hr;.

Avoid using return macros to return success

Avoid using RETURN_XXX macros to return success.

If you do this, it makes it harder to understand "successful" early returns from errors. In other words, treat RETURN_XXX macros like you would treat an exception. Especially once you use these macros for a long time, following this guideline makes it a lot easier to read code.

For example, in this code you can clearly see the control flow for the success case:

RETURN_IF_FAILED(SomeFunction());
if (SomeCondition(operation))
{
    RETURN_IF_FAILED(AnotherFunction());
    RETURN_IF_FAILED(YetAnotherFunction());
}
return S_OK;

Compare this to code that uses RETURN_XXX in the success case:

RETURN_IF_FAILED(SomeFunction());
RETURN_HR_IF(S_OK, !SomeCondition(operation)); // BAD example!!! Don't do this!
RETURN_IF_FAILED(AnotherFunction());
RETURN_IF_FAILED(YetAnotherFunction());
return S_OK;

This second example makes it harder to see the control flow.

Similarly, prefer to return S_OK as the last line of an error code handling function, rather than compressing the last line to a RETURN_HR(MyCall()) macro. It avoids combining possible success with failure.

// Prefer to always conclude an error returning function with an S_OK return
RETURN_IF_FAILED(MyCall());
return S_OK;

The principle of not using return macros for success also explains why there are no macros like GOTO_IF_FAILED.

Using error handling macros to stop processing without exiting the function

If you want to use error-handling macros in part of your code without returning early, we recommend putting these macros inside an immediately-executed lambda. Then, the RETURN_IF_FAILED macros will exit the lambda rather than the enclosing function. You can optionally capture the return value of the lambda to see why the lambda exited.

    const auto result = [&]
    {
        RETURN_IF_FAILED(A());
        RETURN_IF_FAILED_EXPECTED(B());
        RETURN_IF_FAILED(C());
        return S_OK;
    }();

Example: Your function does not return an HRESULT. You can use the facade pattern and throw away the result.

void Something()
{
    // ignore these failures, best effort
    [&]
    {
        RETURN_IF_FAILED(A());
        RETURN_IF_FAILED_EXPECTED(B());
        RETURN_IF_FAILED(C());
        return S_OK;
    }();
}

// This function is documented as returning 0 on failure
// and recording the reason in SetLastError().
int CalculateSomething()
{
    int result = 0;
    // ignore these failures, best effort
    HRESULT hr = [&]
    {
        RETURN_IF_FAILED(PRepare());
        RETURN_IF_FAILED_EXPECTED(Calculate());
        RETURN_IF_FAILED(GetResult(&result));
        return S_OK;
    }();
    if (FAILED(hr))
    {
        result = 0;
    }
    SetLastError(hr);
    return result;
|

Example: Collecting an error code from a sequence of calls that are a part of a larger algorithm.

HRESULT Something()
{
    // Try to do it this way.
    const auto result = [&]
    {
        RETURN_IF_FAILED(A());
        RETURN_IF_FAILED_EXPECTED(B());
        RETURN_IF_FAILED(C());
        return S_OK;
    }();

    // If that failed with a specific error code,
    // then try an alternate way.
    if (result == E_SPECIAL_ERROR_CODE)
    {
        RETURN_IF_FAILED(A_alternate());
        RETURN_IF_FAILED_EXPECTED(B_alternate());
        RETURN_IF_FAILED(C_alternate());
    }
    else
    {
        // Use _EXPECTED here because the failure was already logged
        // by the macro that caused the lambda to exit prematurely.
        RETURN_IF_FAILED_EXPECTED(result);
    }
    return S_OK;
}

Example: A method that for legacy reasons must return S_OK.

HRESULT Something()
{
    // ignore these failures, best effort
    [&]
    {
        RETURN_IF_FAILED(A());
        RETURN_IF_FAILED_EXPECTED(B());
        RETURN_IF_FAILED(C());
        return S_OK;
    }();

    // No matter what happens, always return S_OK.
    return S_OK;
}

Other control flow patterns for errors

Do not create macros like BREAK_IF_FAILED or CONTINUE_IF_FAILED. Write the error handling behavior explicitly:

// Fiddle the knobs on all owners that exist.
for (auto& item : items)
{
    Owner owner;
    IF (FAILED_LOG(item.GetOwner(&owner))
    {
        continue;
    }
    RETURN_IF_FAILED(owner.FiddleKnob());
}

The continue statement is itself problematic, because people may put code at the end of the loop, not realizing that it may be skipped. Consider this alternative:

// Fiddle the knobs on all item owners that exist.
for (auto& item : items)
{
    Owner owner;
    IF (SUCCEEDED_LOG(item.GetOwner(&owner))
    {
        RETURN_IF_FAILED(owner.FiddleKnob());
    }
}

(Another problem with a macro named CONTINUE_IF_FAILED is that it could be confused with the normal English sense of the word "continue" meaning "proceed normally".)

Fail fast based error handling

The FAIL_FAST_XXXX macros eventually utilize a call to the __fastfail intrinsic to terminate the process and generate an error report (this call does not return).

The basic philosophy behind fail fast is simple: find and eliminate more bugs by making failures immediate and visible so that defects are much easier to find and diagnose. The opposite of this would be "failing slowly," as in trying to be resilient to any error, which can lead to more subtle and difficult-to-diagnose bugs.

Prefer fail fast when a program's invariants have been violated or when one of its basic guarantees cannot be met. For example, failure of synchronization primitives or failed communication between required components.

void ThreadingService::EnsureUIThread()
{
    FAIL_FAST_IF(m_uiThreadId != GetCurrentThreadId());
}

In the previous example, fail fast is being used to prevent execution of code off of the UI thread. Preventing execution helps prevent difficult-to-discover bugs where other threads are allowed to touch data that should only be touched by a UI thread. This prevents the UI thread's basic guarantees about data access from being violated.

When to avoid fail fast

Fail fast creates the best possible means for detecting errors and ensuring that code is error-free. Use it when possible, especially for detectable programming errors or other failures unrelated to accessing resources.

With that said, some errors should not lead to fail fast. Specifically, consider avoiding fail fast for:

  • Actions that are stateless (and can't produce indeterminate states); consider scenarios like performing an operation on a file from a file browser - the operation may fail, but that shouldn't fail the file browser.
  • Validation of input across a security boundary; ensure callers cannot cause a denial of service when calling across an API boundary. Input pointers, for example, should be validated as non-null when crossing a security boundary.
  • Lower level code without scenario visibility; code that generically opens a file, for example, cannot know whether the inability to do that is a critical error for it's caller. The caller must decide whether absence of the file or errors reading the file are cause for fail fast.
  • API surfaces which propagate errors to their callers should typically be designed to minimize the conditions where their basic guarantees could not be met. For complicated systems fail fast may still be required, but it should be rare as fail fast in lower level components can cause user data loss in ways that are not easily controllable (other than avoiding use) from calling applications.

Avoid redundant use

Note that you should also avoid using these macros to validate contracts which would naturally lead to a crash anyway when violated:

HRESULT FailFastOverkill(IStorageItem *file)
{
    // This macro is unnecessary (the call will AV anyway)
    FAIL_FAST_IF_NULL(file);
    return file->ReadSomething();
}

Testing fail fast

Fail fast creates difficulties with tests. Fail-fast results in unwind code paths being optimized out of binaries, so tests cannot intercept, log and continue in the face of a fail fast. When creating a unit test for a fail fast condition, the expectation is that the test crashes (and fails).

It may be helpful in that situation to write those tests anyway and run them manually, and disable them in any continuous integration.

Logging errors

The LOG_XXXX macros should be used when the results of an operation can be safely ignored, but the failure should be logged for future inspection.

In new code, logging is usually only desired when an error is going to be ignored. Exception-based code and error-code-based code both propagate errors up the call stack. Consider using logging to avoid losing visibility of errors when that propagation reaches an ABI or contract boundary (thread proc, callback, etc.).

void WINAPI MyCallback() noexcept try
{
    // callback code
}
CATCH_LOG()

Note that you should not use CATCH_LOG() at function level for a destructor. See catch with constructors and destructors.

Instrumenting older code

Logging can safely be added to existing code since it has no side effects. The design enables this to be done with a minimal impact and no changes to control flow. For example this code:

hr = GetCallerProcessId(&processId);

Can be changed as follows to enable logging.

hr = LOG_IF_FAILED(GetCallerProcessId(&processId));

Macro reference

This is a walkthrough of the different types of error handling macros. For the most up-to-date API, see result_macros.h, starting at around line 584.

The error handling macros are designed to be used by functions which either

  • report errors by throwing a C++ exception, or
  • report errors by returning a failure HRESULT.

If your function reports errors in some other way, you can use the facade pattern (described above) to wrap an HRESULT-based immediately-executed lambda, and then convert the failed HRESULT to your desired error-reporting mechanism.

Error handling patterns

WIL provides error handling macros for many common patterns (unconditional failure, conditional failure, GetLastError(), for example).

Each of these patterns can be used with almost every error handling technique (exception-based, fail-fast, early-return, for example) by using a different prefix (THROW_XXXX, RETURN_XXXX, FAIL_FAST_XXXX, or LOG_XXXX). Unless otherwise mentioned, each pattern can be used with every error handling technique (the examples below focus mainly on exception-based macros for simplicity).

Likewise, nearly every macro also has a "message" variant (XXXX_MSG). Every early-return macro (RETURN_) has an "expected error" variant as well (XXXX_EXPECTED).

Unconditionally report a failure

XXXX_HR(hr)
XXXX_LAST_ERROR()
XXXX_WIN32(win32err)
XXXX_NTSTATUS(ntstatus)

These macros all produce unconditional behavior based on the technique being employed - throwing an exception, returning from the calling function, etc. Note that these macros should not be given a success code; on success they will fail-fast (except for RETURN_HR which is used as a pass-through, though this is discouraged).

if (!::ControlService(m_serviceHandle.get(), SERVICE_CONTROL_STOP, &status) &&
    (GetLastError() != ERROR_SERVICE_NOT_ACTIVE))
{
    RETURN_LAST_ERROR();
}
case WAIT_TIMEOUT:
    THROW_WIN32(ERROR_TIMEOUT);
    break;
RETURN_HR(E_ACCESSDENIED);
LOG_NTSTATUS(STATUS_INVALID_DEVICE_REQUEST);

Functions returning an HRESULT

XXXX_IF_FAILED(hr)

Fails with the given HRESULT.

THROW_IF_FAILED(itemArray->GetCount(&count));
FAIL_FAST_IF_FAILED(m_spSession->RemoveListener(this));

Win32 APIs returning a BOOL result where GetLastError must be called on failure

XXXX_IF_WIN32_BOOL_FALSE(win32BOOL)

Fails with HRESULT_FROM_WIN32 of GetLastError if the given BOOL is FALSE. Only supports the Win32 BOOL typedef returned by Win32 APIs and not logical C++ bool. Always ensures the result is a failure to account for rare cases where Win32 APIs can return FALSE without properly setting last error.

THROW_IF_WIN32_BOOL_FALSE(ImpersonateLoggedOnUser(currentToken.get()));
RETURN_IF_WIN32_BOOL_FALSE(CertDeleteCertificateFromStore(pFound));

Win32 APIs directly returning a Win32 error code

XXXX_IF_WIN32_ERROR(win32err)

Fails with HRESULT_FROM_WIN32 of the given Win32 error code if it is anything other than ERROR_SUCCESS.

THROW_IF_WIN32_ERROR(RegOpenCurrentUser(KEY_WRITE, &UserKey));
LOG_IF_WIN32_ERROR(::RegDeleteValueW(registryKey.get(), pszName));

Allocation or resource failure null checks

XXXX_IF_NULL_ALLOC(ptr)

Fails with E_OUTOFMEMORY if the given pointer is null.

m_session = Microsoft::WRL::Make<SessionFactory>(Security::UserToken());
THROW_IF_NULL_ALLOC(m_session);
description = wil::unique_bstr(FAIL_FAST_IF_NULL_ALLOC(::SysAllocString(L"Answer")));

Report a specified HRESULT based upon a condition or null check

XXXX_HR_IF(hr, condition)
XXXX_HR_IF_NULL(hr, ptr)

The first form fails with the given HRESULT if the given logical bool condition is true; the second fails when the given pointer is null.

THROW_HR_IF(E_ACCESSDENIED, !User::IsElevated() && !User::IsLocalSystem());
THROW_HR_IF_NULL(E_ILLEGAL_METHOD_CALL, m_raw);
RETURN_HR_IF(E_HANDLE, !m_event.IsValid());

Report the result of GetLastError based upon a condition or null check

XXXX_LAST_ERROR_IF(condition)
XXXX_LAST_ERROR_IF_NULL(ptr)

The first form fails with HRESULT_FROM_WIN32 of GetLastError if the given logical bool condition is true; the second fails when the given pointer is null. Guarantees failure even if last error is not properly set.

DWORD waitStatus = WaitForSingleObject(pi.hProcess, INFINITE);
THROW_LAST_ERROR_IF(waitStatus == WAIT_FAILED);
m_timer.reset(::CreateThreadpoolTimer(TimerEventCallback, this, nullptr));
THROW_LAST_ERROR_IF_NULL(m_timer);
auto dataSize = GlobalSize(hGlobal);
FAIL_FAST_LAST_ERROR_IF(dataSize == 0);

Features of the error handling macros

Conditional error handling macros serve as an expression

Conditional macros (for example, THROW_IF_FAILED(hr)), other than RETURN_XXX ones, return the same result that was passed to the macro for evaluation, allowing the macro to be treated as an expression.

This allows logging to naturally be inserted within existing code:

hr = LOG_IF_FAILED(FunctionCall());

It also allows inspection of the condition evaluated by the macro:

if (S_FALSE == THROW_IF_FAILED(OldFunctionThatOverloadsReturnValue()))
{
    // ...
}

The conditional RETURN_XXX macros must always exist as a statement to return the error code, rather than an expression, and thus do not share this attribute.

Error handling macros have stronger type checking than straight C++

The macros have all had their type checking strengthened to the maximum possible with C++ given the types being worked with. Win32 BOOL, for example, will not accept C++ bool. C++ logical bool will accept bool, BOOL, boolean, BOOLEAN, and classes with an explicit bool cast. HRESULTs are also limited to signed long values.

This type checking makes it considerably more difficult to use the wrong macro or transpose parameters for existing macros.

Error handling macros using GetLastError will always fail even if there is no last error

Error handling macros such as RETURN_LAST_ERROR_IF(condition) (i.e. those that involve retrieving GetLastError) should only be called when the last error holds a failure. WIL protects against this by reporting ERROR_ASSERTION_FAILURE (and asserting) if a failure macro is called when the last error is not properly set.

The assertions typically happen because:

  1. Your code is using a macro (such as RETURN_IF_WIN32_BOOL_FALSE()) on a function that does not actually set the last error (consult MSDN).
  2. Your macro check against the error is not immediately after the API call. Pushing it later can result in another API call between the previous one and the check resetting the last error.
  3. The API you're calling has a bug in it and does not accurately set the last error (there are a few examples here, such as SendMessageTimeout(...)).

All XXXX_IF_NULL macros directly support smart pointers

RAII types typically support a comparison against nullptr which allows you to directly utilize an error handling macro against the RAII type without calling any form of resource get() routine. Example:

wil::unique_threadpool_timer timer(::CreateThreadpoolTimer(TimerEventCallback, this, nullptr));

// You can do this
THROW_LAST_ERROR_IF_NULL(timer);

// Rather than having to do this
THROW_LAST_ERROR_IF_NULL(timer.get());

Using custom messages

Nearly every macro also has a corresponding XXXX_MSG suffixed version that allows for a generic sprintf style logging message to also be provided when an error is discovered from that particular macro.

The work required to construct the string from the given parameters is not performed unless the macro has discovered a failure, but the format strings make it into the binary and affect binary size, so they should only be used when there are additional parameters, values or concepts that are important to understanding the nature of the error. Avoid using them for information that could be gleaned from the position where the error was generated:

// BAD: Don't do this
RETURN_HR_MSG(E_BAD, "It Broke Here");

// GOOD: when file, line and version information received through logging is sufficient
RETURN_HR(E_BAD);

// BAD: Don't do this - It provides too much/redundant information that increases binary size
// Since file and line number is present you don't need the method name
RETURN_HR_MSG(E_BAD, "MyFileClass:MyFileMethodDealingWithStuff - error dealing with item – "
                     "Name: %ls, Size: %I64u", name, attributes);

// GOOD: Minimum string size, enough context to read the logs
RETURN_HR_MSG(E_BAD, "Name: %ls, Size: %I64u", name, attributes);

Be explicit when logging strings with XXXX_MSG macros. Use either: %ls or %hs to control whether an argument is a wide or ascii string, rather than using %s or %S. The format string is expected to be ASCII to minimize binary size, but internally is printed Unicode to avoid loss of data from params. Use the explicit format specifier to avoid the ambiguity this can bring.

In order to help avoid these types of errors, starting in v1.0.231028.1 the definitions of the XXXX_MSG macros have additionally been updated to include a "dummy" call to wprintf which will get optimized out of the compiled binary. This will force all major compilers to check the variadic arguments passed in to ensure that their types are consistent with the format string used. Because we need to call wprintf here (for proper validation) we must "widen" the format string passed to the macro. This therefore requires that the format string be a string literal, otherwise you will get a somewhat cryptic error message saying something like error C2146: syntax error: missing ')' before identifier '...'. Some common errors you might see as well as possible remediations are:

error C4477: 'wprintf' : format string '...' requires an argument of type '...', but variadic argument N has type '...' 

This is the error message you'll see for legitimate bugs. E.g. using a format string of "%s" for a char*, etc. The fix is to pass the correct format string (e.g. "%hs" for the above example).

error C2146: syntax error: missing ')' before identifier '...' 

This is the error message that you'll see if you don't pass a string literal for the format string. Some examples of causes and possible fixes:

// (1) Using global/constant for the format string 
// BEFORE: 
const char* g_FormatString = "foo"; 
struct Globals { static const char* FormatString = "foo"; }; 
LOG_HR_MSG(E_FAIL, g_FormatString); 
LOG_HR_MSG(E_FAIL, Globals::FormatString); 
// AFTER: 
LOG_HR_MSG(E_FAIL, "%hs", g_FormatString); 
LOG_HR_MSG(E_FAIL, "%hs", Globals::FormatString); 
    // OR 
#define FORMAT_STRING "foo" 
LOG_HR_MSG(E_FAIL, FORMAT_STRING); 

// (2) Using local/member variable for the format string 
// BEFORE: 
LOG_HR_MSG(E_FAIL, exception.what()); 
LOG_HR_MSG(E_FAIL, m_errorMsg.c_str()); 
// AFTER: 
LOG_HR_MSG(E_FAIL, "%hs", exception.what()); 
LOG_HR_MSG(E_FAIL, "%hs", m_errorMsg.c_str()); 

// (3) Selecting between different format strings 
// BEFORE: 
LOG_HR_MSG(E_OUTOFMEMORY, fatal ? "Fatal allocation failure. Size=%zu" : "Non-fatal allocation failure. Size=%zu", size); 
// AFTER: 
fatal ? 
    LOG_HR_MSG(E_OUTOFMEMORY, "Fatal allocation failure. Size=%zu", size) : 
    LOG_HR_MSG(E_OUTOFMEMORY, "Non-fatal allocation failure. Size=%zu", size); 

// (4) Using global/constant for the format string w/ args 
// BEFORE: 
const char* g_FormatString = "Operation failed with type %ls"; 
LOG_HR_MSG(E_FAIL, g_FormatString, type); 
// AFTER: 
#define FORMAT_STRING "Operation failed with type %ls" 
LOG_HR_MSG(E_FAIL, FORMAT_STRING, type); 
    // OR 
wchar_t buffer[256]; 
::swprintf(buffer, ARRAYSIZE(buffer), g_FOrmatString, type); 
LOG_HR_MSG(E_FAIL, "%ls", buffer);

If needed, there is also a global opt-out. You can build with 'WIL_NO_MSG_FORMAT_CHECKS' defined to disable the check.

In general, use XXXX_MSG macros sparingly as they unnecessarily add noise to the source and grow binaries. Use them only when required to gain more insight on important failures.

Using custom exceptions

You can use a custom exception with WIL when you want to both associate additional context with an error that will be caught and inspected and you want to precisely control the error code that would be generated when the exception is handled by an exception barrier.

Defining your custom exception type:

class AbortException : public wil::ResultException
{
public:
    AbortException(int code) : ResultException(E_ABORT), abortCode(code) {}
    int abortCode;
};

Throwing your exception with WIL (this associates additional context about where the error occurred):

if (IsAborted())
{
    THROW_EXCEPTION(AbortException(GetAbortCode()));
}

Catching and using your exception:

try
{
    // Perform operation...
}
catch (const AbortException& ex)
{
    ReportAbort(ex.abortCode);
}

If your custom exception types do not derive from wil::ResultException, see Custom exception types for further details on how to add support them.

Exception Guards

An exception guard (also known as an exception barrier) is a tool to prevent exceptions from crossing a boundary that expects an error code response or one that does not expect exceptions to be thrown.

WIL provides several forms of exception guard. The first and primary recommended mechanism is through the traditional use of try/catch and macros to do conversion of exceptions to HRESULTs:

HRESULT ErrorCodeReturningFunction() try
{
    // Code ...
    return S_OK;
}
CATCH_RETURN();

WIL's exception barriers (including the CATCH_RETURN() macro above) catch ALL exceptions, log them, and convert them to an HRESULT. Supported exception types are:

Exception HRESULT Notes
wil::ResultException ex.GetErrorCode()
Platform::Exception^ ex->HResult If compiling as C++/CX
winrt::hresult_error ex.to_abi() Must #include <wil/cppwinrt.h>
std::bad_alloc E_OUTOFMEMORY
std::out_of_range E_BOUNDS Must #include <wil/cppwinrt.h>
std::invalid_argument E_INVALIDARG Must #include <wil/cppwinrt.h>
std::exception HRESULT_FROM_WIN32(
ERROR_UNHANDLED_EXCEPTION)

If you #include <wil/cppwinrt.h>, you must do so before including any C++/WinRT headers.

Clients can add support for additional exception types. See Custom exception types for further details.

Any unsupported exception type that does not itself derive from std::exception will fail-fast by default. These are typically the result of accidents such as throw E_FAIL; where the caller mistakenly just throws an error code or a non-exception based object. These are generally programming errors. (See Error handling customization to alter the default fail-fast behavior.)

Exception guards (other than noexcept or explicit fail fast) are not generally recommended for constructors or destructors. Dealing with half-constructed objects or half-destroyed objects can cause errors and can usually be avoided. Function-level-try should also be avoided in constructors and destructors.

Guard macros and helpers

Just like the normal error handling macros, there are macro variants to handle exceptions in each of the four primary techniques:

CATCH_RETURN()
CATCH_LOG()
CATCH_FAIL_FAST()
CATCH_THROW_NORMALIZED()

CATCH_RETURN will generally be the most commonly used guard macro due to the abundance of interop with HRESULTs done on ABI boundaries.

When using function-level CATCH_LOG after a destructor, prefer CATCH_LOG_RETURN() instead, as CATCH_LOG implicitly rethrows at the end of its scope in these cases.

At times, it may be advantageous to first examine an error for some purpose prior to allowing the exception guard to log a message and return the proper result. For these purposes, there are separate macros that allow dealing with exceptions which have already been caught (when you are in a catch block):

RETURN_CAUGHT_EXCEPTION()
LOG_CAUGHT_EXCEPTION()
FAIL_FAST_CAUGHT_EXCEPTION()
THROW_NORMALIZED_CAUGHT_EXCEPTION()

For these, use the following pattern:

try
{
    // code
}
catch (...) /* `...` is the catch-all handler */
{
    // code
    RETURN_CAUGHT_EXCEPTION();
}

If you need to know what the error result is within the catch block, you can use the ResultFromCaughtException function to examine it:

try
{
    // code
}
catch (...) /* `...` is the catch-all handler */
{
    if (wil::ResultFromCaughtException() == SQLITE_E_CORRUPT)
    {
        // code
    }
    RETURN_CAUGHT_EXCEPTION();
}

If the function must not throw exceptions, and it does not return HRESULT, you can use a custom catch block to transform the exception to the desired return value.

// This function is documented as returning 0 if unable to calculate,
// with the reason recorded as SetLastError.
int CalculateSomething() try
{
    Prepare();
    int result = Calculate();
    Finish();
    SetLastError(NO_ERROR); // in case result was validly zero
    return result;
}
catch (...)
{
    LOG_CAUGHT_EXCEPTION();
    SetLastError(wil::ResultFromCaughtException());
    return 0;
}

Function based guards

WIL provides function-based exception guards, also known as exception barriers, for diagnosing failures which are difficult to reproduce.

Typically, exception origination is diagnosed using a debugger, enabling the option to break on the first chance stage of exception dispatch for specific exception types. This stops execution at the point the exception is thrown (the exception's "origin"), rather than when it is handled.

For failures that can't be reproduce under the debugger, the fail fast and debug exception guards convert exceptions into fail-fast at the origin of the exception, instead of the location of where it is caught after the stack has unwound. This is implemented using structured exception handling.

In general, you should use traditional try/catch because it optimizes better and reduces boilerplate to step through when debugging, but these function-based guards assist in diagnosing failures which are difficult to reproduce.

Function-based guards are often used during development and are removed once the issue has been resolved. But there cases where exceptions are not tolerated, in which case you may also find it used in release code.

Note that noexcept will produce the same result and since that is a language feature is now preferred over these guards.

void MustNotFail() noexcept
{
    // Use of libraries or function calls that may throw
    // exceptions...
}
void MustNotFail()
{
    wil::FailFastException(WI_DIAGNOSTICS_INFO, [&]()
    {
        // Use of libraries or function calls that may throw
        // exceptions...
    });
}

The code above has a clear advantage in diagnosis over a try/CATCH_FAIL_FAST as it will terminate at the point the exception is thrown, rather than at the point of the CATCH_FAIL_FAST.

Similarly, you can also use a function-based guard to convert exceptions to HRESULTs as follows:

HRESULT ErrorCodeBasedFunction() noexcept
{
    return wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&]()
    {
        // Use of libraries or function calls that may throw
        // exceptions...
    });
}

wil::ResultFromExceptionDebug() provides a conditional fail-fast for certain exception types. For example, the following example will log a message and convert allocation errors or errors thrown with WIL to an HRESULT, but will use the same structured exception handling insertion to fail-fast any other exception type:

HRESULT ErrorCodeBasedFunction() noexcept
{
    return wil::ResultFromExceptionDebug(WI_DIAGNOSTICS_INFO, wil::SupportedExceptions::ThrownOrAlloc, [&]()
    {
        // Use of libraries or function calls that may throw
        // exceptions...
    });
}

This is useful for identifying the source of specific hard to reproduce exceptions.

Remapping exception codes

By default, WIL has visibility into the error codes for std::bad_alloc and any error codes generated through use of a WIL macro or C++/CX's Platform::Exception^. WIL also handles std::exception based errors generically, remapping them into HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION) (0x8007023e). Though std::exception exceptions are lumped into a single error code, WIL does preserve their what() string in the log message to help identify the actual exception type and problem.

It's not strictly recommended as it creates a DLL-based dependency, but for components that want to further remap some std::exception based errors into a particular error code, it's possible to do by setting up a global exception remapping function. This example adds mapping of std::bad_weakref to E_POINTER:

HRESULT MyDllResultFromCaughtException() WI_NOEXCEPT
{
    try
    {
        throw;
    }
    catch (std::bad_weakref&)
    {
        return E_POINTER;
    }
    catch (...)
    {
    }
    // return S_OK when *unable* to remap the exception
    return S_OK;
}

BOOL WINAPI DllMain(HANDLE, DWORD dwReason, LPVOID)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        // Plug in additional exception-type support
        wil::g_pfnResultFromCaughtException = &MyDllResultFromCaughtException;
        break;
    }
    return 1;
}

Usage issues

Interaction between macros and template function parameter use of commas

Since most of the error handling helpers are macros, they can occasionally suffer from undesired interactions with the C++ language. Specifically, a comma either in a template argument list or as an operator may be mistaken as a macro parameter separator. For example, the following code will fail to compile due to the presence of the comma in the template argument list:

THROW_IF_FAILED(Function<1, 2>());

warning C4002: too many actual parameters for macro 'THROW_IF_FAILED’

The workaround is to include an extra set of parentheses. In this case:

THROW_IF_FAILED((Function<1, 2>()));

Interaction with lambdas

Avoid wrapping function calls accepting lambdas with error handling helper macros. Macros collapse their bodies into a single line, which impairs debuggability when the body of the macro extends over multiple lines of source code.

With the following example, you will be unable to set breakpoints inside the lambda because the preprocessor will collapse the entire macro body into one line:

RETURN_IF_FAILED(AddTaskToQueue([]() noexcept // bad, don't do this
    {
        DoSomething();
        DoSomethingElse();
    });

To avoid this problem, separate the lambda from the macro examining the function result:

const auto addResult = AddTaskToQueue([]() noexcept
    {
        DoSomething();
        DoSomethingElse();
    });
RETURN_IF_FAILED(addResult);

Function-level try/catch with constructors and destructors

Use function-level try/catch freely everywhere other than constructors and destructors.

Exceptions cannot be silently ignored (caught and logged or handled) from a function-level try/catch block on constructors and destructors (this is different behavior from other functions). The following example will always throw when the object is constructed, even though it looks like all exceptions are being handled:

class MyClass
{
public:
    MyClass() try
    {
        // some code that throws
    }
    catch (...)
    {
        // the caught exception will be automatically rethrown after this block is executed
    }
};

The best practice is to avoid try/catch blocks entirely in constructors and destructors, but if one is necessary to catch and ignore or log errors, use a try/catch block within the function itself:

class MyClass
{
public:
    MyClass() noexcept
    {
        try
        {
            // some code that throws
        }
        CATCH_LOG();
    }
};

Do not depend on logging callbacks for awareness of errors

Avoid using wil::ThreadFailureCallback, wil::SetResultLoggingCallback, or wil::SetResultTelemetryFallback for any code that needs to deterministically understand whether a failure has happened or not.

For example, don't do this:

// Clean up on failure, otherwise we leak (circular reference).
auto monitor = wil::ThreadFailureCallback([this](wil::FailureInfo const & /*failure*/) noexcept
{
    ReportFailureToCustomLog(failure);   // Good, intended use
    BreakCircularReferencesAndCleanUp(); // Bad, don't do this
    return false;
});

The code above puts clean-up logic within the callback. This will miss both suppressed errors and errors originating from sources other than WIL error handling helpers (Platform::Exception^ for example). It also means that anyone who disables logging may unintentionally create a circular reference bug by silencing the callback.

In this particular example, it would be better to clean up via scope_exit:

// Clean up on failure, otherwise we leak (circular reference).
auto monitor = wil::scope_exit([this]
{
    BreakCircularReferencesAndCleanUp();
});

And then add:

monitor.release();

at the end of the function to prevent making the call when successful. Success is deterministic; the failure callback is not.

See also

Clone this wiki locally