Exposing Electron errors and error conditions to DotNet
I've developed some of this locally to validate the ideas but feel like this proposal has some large unknowns that should be discussed prior to adoption or implementation thus... please tear this apart with me.
One of the things that creates a little bit of a blocker for me as a dotnet developer using Electron.NET is that errors that occur in the electron side of Electron.NET are effectively swallowed. This lack of visibility leads to a lot of time spent diagnosing where an issue occurred (Electron or dotnet) even before figuring out why an issue occurred. To promote visibility of errors in electron I propose the following:
-
For the case where an error occurs and the dotnet side of code is expecting a response but will not receive one:
return a pre-formatted string with the exception information as the content that the dotnet handler can properly parse and throw a .net exception for. -
For the case where an error occurs and the dotnet side of code is not expecting a response:
Create a channel to except exceptions from the js into the dotnet code. This would be a generic channel to accept the messages from any location we devise.
Electron Implementation
On the electron side I propose the addition of the following:
postError.ts
import { Socket } from 'net';
let electronSocket: Socket;
export = (socket:Socket):(message:any, channel: string) => void => {
electronSocket = socket;
return (message: any, channel: string):void => {
let e: Error;
if (message instanceof Error) {
e = message;
}
else {
try {
throw new Error(message);
}
catch (error) {
e = error;
}
}
var errorMessage = `ElectronNETError: ${e.name}: ${e.message}${(e.stack ? `\n--stacktrace--\n${e.stack}` : '')}`
electronSocket.emit(channel || 'unhandled-electronnet-error', errorMessage);
};
}
How this would be consumed in the electron side.
// from browserWindows.ts
export = (socket: Socket, app: Electron.App) => {
electronSocket = socket;
const postError = require('./postError')(socket);
...
// dotnet is expecting a response here
socket.on('browserWindowHasShadow', (id) => {
const w = getWindowById(id);
const channel = 'browserWindow-hasShadow-completed';
if (w) {
const hasShadow = w.hasShadow();
electronSocket.emit(channel, hasShadow);
}
else {
postError(`window with id ${id} was not found`, channel);
}
});
// dotnet is not expecting a response
socket.on('browserWindowBlur', (id) => {
const w = getWindowById(id);
if (w) {
w.blur();
}
else {
postError(`window with id ${id} was not found`);
}
});
// alternatively we could do
socket.on('browserWindowBlur', (id) => {
try {
getWindowById(id).blur();
} catch (e) {
postError(e);
}
});
...
}
DotNet Implementation
On the dotnet side we would have a capture class
using System;
using System.Threading.Tasks;
namespace ElectronNET.API
{
/// <summary></summary>
public static class ErrorCapture
{
private class JsError
{
public string Type { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
}
private static object Extract(object value)
{
if (value is string s && s.StartsWith("ElectronNETError: "))
{
var parts = s.Substring(17).Split("\n--stacktrace--\n");
var typeSplit = parts[0].IndexOf(':');
var type = parts[0].Substring(0, typeSplit);
var message = parts[0].Substring(typeSplit + 1);
var stackTrace = parts.Length > 1 ? parts[parts.Length - 1] : string.Empty;
return new JsError
{
Type = type,
Message = message,
StackTrace = stackTrace,
};
}
return value;
}
/// <summary>
/// delegate for Capture
/// </summary>
public delegate Action<string, string, string> OnError(string type, string message, string stacktrace);
/// <summary>
/// Wrapper for functions to capture exceptions from Electron
/// </summary>
/// <exception cref="ElectronException"></exception>
public static Action<object> Capture(OnError onError)
{
void Handler(object value)
{
if (Extract(value) is JsError e)
{
onError(e.Type, e.Message, e.StackTrace);
}
}
return Handler;
}
/// <summary>
/// Wrapper for functions to capture exceptions from Electron
/// </summary>
/// <exception cref="ElectronException"></exception>
public static Action<object> Capture(Action<object> action)
{
void Handler(object value)
{
if (Extract(value) is JsError e)
{
throw new ElectronException(e.Message, e.Type, e.StackTrace);
}
action?.Invoke(value);
}
return Handler;
}
/// <summary>
/// Wrapper for functions to capture exceptions from Electron
/// </summary>
/// <exception cref="ElectronException"></exception>
public static Action<object> Capture<T>(Action<object> action, TaskCompletionSource<T> tsc)
{
void Handler(object value)
{
if (Extract(value) is JsError e)
{
try
{
throw new ElectronException(e.Message, e.Type, e.StackTrace);
}
catch (Exception ex)
{
tsc.TrySetException(ex);
return;
}
}
action?.Invoke(value);
}
return Handler;
}
/// <summary>
/// Wrapper for functions to capture exceptions from Electron
/// </summary>
/// <exception cref="ElectronException"></exception>
public static Action<object> Capture(Action<object> action, TaskCompletionSource tsc)
{
void Handler(object value)
{
if (Extract(value) is JsError e)
{
try
{
throw new ElectronException(e.Message, e.Type, e.StackTrace);
}
catch (Exception ex)
{
tsc.TrySetException(ex);
return;
}
}
action?.Invoke(value);
}
return Handler;
}
}
/// <inheritdoc />
public class ElectronException : Exception
{
/// <summary>
/// The type of javascript error
/// </summary>
public string JsType { get; }
/// <summary>
/// The javascript stacktrace
/// </summary>
public string JsStackTrace { get; }
/// <inheritdoc />
public ElectronException(string message, string type, string stackTrace)
{
JsType = type;
JsStackTrace = stackTrace;
}
public override string ToString()
=> $@"{base.ToString()}
---From Electron---
{JsType}: {Message}
{JsStackTrace}
";
}
}
API classes that have a callback would be updated to follow this format:
public class BrowserWindow
{
...
public Task<bool> HasShadowAsync()
{
var taskCompletionSource = new TaskCompletionSource<bool>();
BridgeConnector.Socket.On("browserWindow-hasShadow-completed", ErrorCapture.Capture(hasShadow => {
BridgeConnector.Socket.Off("browserWindow-hasShadow-completed");
taskCompletionSource.SetResult((bool)hasShadow);
}, taskCompletionSource));
BridgeConnector.Socket.Emit("browserWindowHasShadow", Id);
return taskCompletionSource.Task;
}
...
}
Client applications would capture unhandled exceptions like this:
Electron.App.On("unhandled-electronnet-error", ErrorCapture.Capture((type, message, stacktrace) =>
{
// do something
});
Final Thoughts
Doing this would mean a large change in the code. We would have to hit many places in both the dotnet and js side of things. This would also mean a large and possibly breaking change for any consumer code as Exception Handling on the .net side would become more paramount. However I believe that the final outcome would be more visibility to the primary developers, i.e. all us dotnet monkeys.
D Gidman