Error Handling
#Script uses Exceptions for error flow control flow that by default will fail fast by emitting the Exception details at the point where the error occured before terminating the Stream and rethrowing the Exception which lets you for instance catch the Exception in Unit Tests.
Handled Exceptions
You can instead choose to handle exceptions and prevent them from short-circuiting page rendering by assigning them to an argument using either the assignError method which will capture any subsequent Exceptions thrown on the page:
catchError
Alternatively you can specify `catchError` at each call-site where it will capture the Exception thrown by the method:
An easy way to prettify errors in Web Pages is to use HTML Error Scripts.
ifErrorReturn
You can use ifErrorReturn
in all Script Methods that throw Managed Exceptions
to specify a return value to use instead of throwing the Exception.
E.g. this can be used to catch the Unauthorized
Exception thrown when the Authenticate
Service is called from an unauthenticated Request, e.g:
{{#script}}
AUTH = {{ 'Authenticate' |> execService({ ifErrorReturn: "null" }) |> json }};
{{/script}}
Unhandled Exceptions Behavior
If Exceptions are unassigned they're considered to be unhandled. The default behavior for ScriptContext is to rethrow Exceptions, but as ServiceStack's SharpPagesFeature is executed within the context of a HTTP Server it's default is configured to:
new ScriptContext {
SkipExecutingFiltersIfError = true
}
Which instead captures any unhandled Exceptions in PageResult.LastFilterError and continues rendering the page except it skips evaluating any subsequent methods and instead only evaluates the methods which handle errors, currently:
ifError |
Only execute the expression if there's an error: {{ ifError |> select: FAIL! { it.Message } }} |
---|---|
lastError |
Returns the last error on the page or null if there was no error, that's passed to the next method: {{ lastError |> ifExists |> select: FAIL! { it.Message } }} |
htmlError | Display detailed htmlErrorDebug when in DebugMode otherwise display a more appropriate end-user htmlErrorMessage. |
htmlErrorMessage | Display the Exception Message |
htmlErrorDebug | Display a detailed Exception |
This behavior provides the optimal UX for developers and end-users as HTML Pages with Exceptions are still rendered well-formed whilst still being easily able to display a curated error messages for end-users without developers needing to guard against executing methods when Exceptions occur.
Controlling Unhandled Exception Behavior
We can also enable this behavior on a per-page basis using the skipExecutingFiltersOnError method:
Here we can see that any normal methods after exceptions are never evaluated unless they're specifically for handling Errors.
Continue Executing Methods on Unhandled Exceptions
We can also specify that we want to continue executing methods in which case you'll need to manually guard methods you want to control the execution of using the ifNoError or ifError methods:
Accessing Page Exceptions
The last Exception thrown are accessible using the lastError* methods:
Throwing Exceptions
We've included a few of the popular Exception Types, methods prefixed with throw always throws the Exceptions below:
Method | Exception |
---|---|
throw | Exception(message) |
throwArgumentException | ArgumentException(message) |
throwArgumentNullException | ArgumentNullException(paramName) |
throwNotSupportedException | NotSupportedException(message) |
throwNotImplementedException | NotImplementedException(message) |
throwUnauthorizedAccessException | UnauthorizedAccessException(message) |
throwFileNotFoundException | FileNotFoundException(message) |
throwOptimisticConcurrencyException | OptimisticConcurrencyException(message) |
You can extend this list with your own custom methods, see the Error Handling Methods for examples.
These methods will only throw if a condition is met:
Method | Exception |
---|---|
throwIf | message |> Exception |> if(test) |
throwArgumentNullExceptionIf | paramName |> ArgumentNullException |> if(test) |
ifthrow | if(test) |> Exception(message) |
ifThrowArgumentNullException | if(test) |> ArgumentNullException(paramName) |
ifThrowArgumentException |
if(test) |> ArgumentException(message) if(test) |> ArgumentException(message, paramName) |
Ensure Argument Helpers
The ensureAll* methods assert the state of multiple arguments where it will either throw an Exception unless all the arguments meet the condition or return the Object Dictionary if all conditions are met:
The ensureAny* methods only requires one of the arguments meet the condition to return the Object Dictionary:
Fatal Exceptions
The Exception Types below are reserved for Exceptions that should never happen, such as incorrect usage of an API where it would've resulted in a compile error in C#. When these Exceptions are thrown in a method or a page they'll immediately short-circuit execution of the page and write the Exception details to the output. The Fatal Exceptions include:
- NotSupportedException
- NotImplementedException
- TargetInvocationException
Rendering Exceptions
The OnExpressionException delegate in Page Formats are able to control how Exceptions in #Script expressions are rendered where if preferred exceptions can be rendered in-line in the template expression where they occurred with:
var context = new ScriptContext {
RenderExpressionExceptions = true
}.Init();
The OnExpressionException can also suppress Exceptions from being displayed by capturing any naked Exception Types registered in TemplateConfig.CaptureAndEvaluateExceptionsToNull and evaluate the template expression to null which by default will suppress the following naked Exceptions thrown in methods:
- NullReferenceException
- ArgumentNullException
Implementing Method Exceptions
In order for your own Method Exceptions to participate in the above Script Error Handling they'll need to be wrapped in an StopFilterExecutionException including both the Script's scope and an optional options object which is used to check if the assignError binding was provided so it can automatically populate it with the Exception.
The easiest way to Implement Exception handling in methods is to call a managed function which catches all Exceptions and throws them in a StopFilterExecutionException as seen in OrmLite's DbScripts:
T exec<T>(Func<IDbConnection, T> fn, ScriptScopeContext scope, object options)
{
try
{
using (var db = DbFactory.Open())
{
return fn(db);
}
}
catch (Exception ex)
{
throw new StopFilterExecutionException(scope, options, ex);
}
}
public object dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args) =>
exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, null);
public object dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args, object op) =>
exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, op);
public object dbSingle(ScriptScopeContext scope, string sql, Dictionary<string, object> args) =>
exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, null);
public object dbSingle(ScriptScopeContext scope, string sql, Dictionary<string, object> args, object op) =>
exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, op);
The overloads are so the methods can be called without specifying any method options.
For more examples of different error handling features and strategies checkout: ErrorHandlingTests.cs