-
Notifications
You must be signed in to change notification settings - Fork 236
Error handling helpers
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
To use WIL error handling helpers, add wil/result.h
to your C++ sources:
#include <wil/result.h>
The macros referenced below are defined in
wil/result_macros.h
,
which is also included in result.h
.
By default, WIL error handling helpers aren't aware of C++/WinRT's
exception types, and will default to fail-fast messages for these
exceptions. You can fix this by including wil/cppwinrt.h
before including any C++/WinRT header files in your source file.
This will also turn on other integrations between WIL and
C++/WinRT together.
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:
- Prefer to handle errors with exceptions when possible; this enables full use of exception-based libraries
- Otherwise use uniform error code propagation
- Always use fail-fast when your program invariants have been violated (unrecoverable)
- Consider using fail-fast to handle other classes of errors if your code can accommodate it.
- 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 whom 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.
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.
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.
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.
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();
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
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
).
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_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
.
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;
}();
}
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;
}
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".)
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.
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.
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();
}
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.
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.
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));
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.
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
).
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);
XXXX_IF_FAILED(hr)
Fails with the given HRESULT.
THROW_IF_FAILED(itemArray->GetCount(&count));
FAIL_FAST_IF_FAILED(m_spSession->RemoveListener(this));
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));
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));
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")));
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());
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);
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.
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 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:
- 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). - 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.
- 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(...)
).
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());
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 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.
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);
}
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:
-
Platform::Exception^
with the exception's HRESULT -
wil::ResultException
with the exception's HRESULT -
std::bad_alloc
with E_OUTOFMEMORY -
std::exception
derived exception with HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION) == 0x8007023e - C++/WinRT exception with the exception's HRESULT
(only if
wil/cppwinrt.h
is included before any C++/WinRT header files)
Any unsupported exception type that does not itself derive from
std::exception will fail-fast. 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).
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.
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();
}
WIL also makes several function-based exception guards available. Though not generally recommended over traditional try/catch (they do not optimize as well and provide more boilerplate to step through when debugging) these guards do provide some unique capabilities that can aid debugging and discovery of issues.
The first capability worth mentioning is the guard's ability to used structured exception handling to fail fast at the point an exception is thrown (rather than at the location it's caught after the stack has unwound). This is very useful when converting an exception to fail-fast:
void MustNotFail() noexcept
{
wil::FailFastException(WI_DIAGNOSTICS_INFO, [&]()
{
// Use of libraries or function calls that may throw
// exceptions...
});
}
This 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 exception 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...
});
}
This pattern, though, can be overridden with the 'Debug' suffix (ResultFromExceptionDebug) to provide a conditional fail-fast for certain exception type to help with discovery of errors. 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 kind of pattern can be very useful to insert to help ease identifying the source of problematic unknown exceptions.
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;
}
The error handling helpers split log messages into 2 groups:
- "telemetry" messages
- non-"telemetry" messages
(What is the distinction between telemetry and non-telemetry? This needs to be added.)
The terminology comes from the original Windows-internal version of WIL, which sends telemetry messages to Windows' built-in telemetry uploader.
In the current open-source WIL, both telemetry and non-telemetry messages
are written to the debugger output with OutputDebugString
if a user-mode debugger is attached to the app process,
and ignored otherwise.
So if you have a program like this:
// Example only, not necessarily adhering to recommended practices.
#include <exception>
#include <Windows.h>
#include "wil/result.h"
void BlowUp() try
{
throw std::exception{ "Too bad!" };
} CATCH_LOG()
int main()
{
LOG_HR(E_UNEXPECTED);
BlowUp();
}
then when you run it under a debugger you will see WIL log messages like these:
X:\scratch.cpp(12)\scratch.exe!00073551: (caller: 00074A6C) LogHr(1) tid(4ac0) 8000FFFF Catastrophic failure
X:\scratch.cpp(8)\scratch.exe!00073512: (caller: 00073559) LogHr(2) tid(4ac0) 8007023E {Application Error}
The exception %s (0x Msg:[std::exception: Too bad!]
You can add to the default logging behavior in limited ways by registering callback functions with WIL, but be careful: don't write code in these callback functions that affects your code's logic.
Use wil::ThreadFailureCallback
to add a thread-local callback function
that WIL will call before outputting each error handling helper log message.
You can register as many callbacks as you like with ThreadFailureCallback;
each call to ThreadFailureCallback returns an RAII object that will
automatically unregister the callback when it goes out of scope.
auto monitor = wil::ThreadFailureCallback([](wil::FailureInfo const &failure) noexcept
{
constexpr int failureLimit = 100;
if (failure.failureId > failureLimit)
{
// This module (.dll or .exe) has failed too many times.
// Better tell someone, quick!
}
});
Use wil::SetResultLoggingCallback
to set a single process-wide callback function
that WIL will call before outputting each log message; if you're writing the
top-level code of an application, you might place a call to your preferred
logging library here.
// Print every log message to standard error until SetResultLoggingCallback()
// is called with a different callback.
wil::SetResultLoggingCallback([](wil::FailureInfo const &failure) noexcept
{
constexpr std::size_t sizeOfDebugStringWithNul = 2048;
wchar_t debugString[sizeOfDebugStringWithNul];
wil::GetFailureLogString(debugString, sizeOfDebugStringWithNul, failure);
std::fputws(debugString, stderr);
std::fputwc(L'\n', stderr);
});
wil::SetResultTelemetryFallback
is similar to SetResultLoggingCallback,
but is only for telemetry messages:
wil::SetResultLoggingCallback([](bool alreadyReportedToTelemetry, wil::FailureInfo const &failure) noexcept
{
// ...
});
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>()));
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);
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();
}
};
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.