Errors
This page covers how F# exceptions and JavaScript errors meet: catching a JavaScript TypeError or a thrown string in F#, making F# exceptions real Error objects for stack traces and error reporters, and raising DivideByZeroException as .NET does. You need it whenever F# code calls JavaScript that can throw, or JavaScript code catches what F# throws.
Catching JavaScript errors by type
Fickl.Core.JS binds the errors JavaScript throws: JS.Error, JS.TypeError, JS.RangeError, JS.SyntaxError, JS.ReferenceError, JS.AggregateError and JS.DOMException. Each is the JavaScript global of the same name, so :? JS.SyntaxError compiles to instanceof SyntaxError. They have message, name, stack and cause.
open Fable.Core
open Fickl.Core
let tryParse (text: string) =
try
Some (JS.JSON.parse text)
with :? JS.SyntaxError as e ->
printfn "%s: %s" e.name e.message
None
import { printf, toConsole } from "@fable-org/fable-library-js/String.js";
import { some } from "@fable-org/fable-library-js/Option.js";
export function tryParse(text) {
try {
return some(JSON.parse(text));
}
catch (matchValue) {
if (matchValue instanceof SyntaxError) {
const e = matchValue;
const arg = e.name;
const arg_1 = e.message;
toConsole(printf("%s: %s"))(arg)(arg_1);
return undefined;
}
else {
throw matchValue;
}
}
}
import { printf, toConsole } from "@fable-org/fable-library-ts/String.ts";
import { type Option, some } from "@fable-org/fable-library-ts/Option.ts";
export function tryParse(text: string): Option<any> {
try {
return some(JSON.parse(text));
}
catch (matchValue: any) {
if (matchValue instanceof SyntaxError) {
const e = matchValue as SyntaxError;
const arg: string = e.name;
const arg_1: string = e.message;
toConsole(printf("%s: %s"))(arg)(arg_1);
return undefined;
}
else {
throw matchValue;
}
}
}
You can also construct and raise them. raise (JS.RangeError "...") compiles to throw new RangeError(...), which JavaScript callers catch as a plain RangeError.
open Fickl.Core
let checkIndex (index: int) (length: int) =
if index < 0 || index >= length then
raise (JS.RangeError $"index {index} is outside 0..{length - 1}")
export function checkIndex(index, length) {
if ((index < 0) ? true : (index >= length)) {
throw new RangeError(`index ${index} is outside 0..${length - 1}`);
}
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
export function checkIndex(index: int32, length: int32): void {
if ((index < 0) ? true : (index >= length)) {
throw new RangeError(`index ${index} is outside 0..${length - 1}`);
}
}
JS.AggregateError has the errors it holds, and JS.DOMException is told apart by its name, such as "AbortError" or "NotFoundError". The bindings inherit exn only so that a handler can test for them: an F# exception is not a JS.Error unless it is compiled as one, as described below.
Thrown values that are not errors
JavaScript can throw anything: a string, a plain object, undefined. The JS.Thrown active pattern matches a caught value that is neither a JavaScript Error nor an F# exception, and passes it as it was thrown. Put it after the type tests.
open Fable.Core
open Fickl.Core
[<Global>]
let runPlugin () : unit = jsNative
let classify () =
try
runPlugin ()
"ok"
with
| :? JS.TypeError as e -> "TypeError: " + e.message
| :? JS.DOMException as e when e.name = "AbortError" -> "aborted"
| :? JS.Error as e -> e.name
| JS.Thrown value -> "thrown " + string value
| e -> e.Message
import { toStringOrEmpty } from "@fable-org/fable-library-js/Types.js";
import { isException, exceptionMessage } from "@fable-org/fable-library-js/Util.js";
import { value as value_1, some } from "@fable-org/fable-library-js/Option.js";
export function classify() {
try {
runPlugin();
return "ok";
}
catch (matchValue) {
let matchResult, e_1, e_2, e_3, value, e_4;
if (matchValue instanceof TypeError) {
matchResult = 0;
e_1 = matchValue;
}
else if (matchValue instanceof DOMException) {
if (matchValue.name === "AbortError") {
matchResult = 1;
e_2 = matchValue;
}
else {
matchResult = 2;
e_3 = matchValue;
}
}
else if (matchValue instanceof Error) {
matchResult = 2;
e_3 = matchValue;
}
else {
const activePatternResult = isException(matchValue) ? undefined : some(matchValue);
if (activePatternResult != null) {
matchResult = 3;
value = value_1(activePatternResult);
}
else {
matchResult = 4;
e_4 = matchValue;
}
}
switch (matchResult) {
case 0:
return "TypeError: " + e_1.message;
case 1:
return "aborted";
case 2:
return e_3.name;
case 3:
return "thrown " + toStringOrEmpty(value);
default:
return exceptionMessage(e_4);
}
}
}
import { toStringOrEmpty } from "@fable-org/fable-library-ts/Types.ts";
import { isException, type Exception, exceptionMessage } from "@fable-org/fable-library-ts/Util.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { value as value_1, type Option, some } from "@fable-org/fable-library-ts/Option.ts";
export function classify(): string {
let e: DOMException = undefined as any;
try {
runPlugin();
return "ok";
}
catch (matchValue: any) {
let matchResult: int32 = (undefined as any), e_1: TypeError = (undefined as any), e_2: DOMException = (undefined as any), e_3: Error = (undefined as any), value: any = (undefined as any), e_4: Exception = (undefined as any);
if (matchValue instanceof TypeError) {
matchResult = 0;
e_1 = matchValue;
}
else if (matchValue instanceof DOMException) {
if ((e = matchValue, e.name === "AbortError")) {
matchResult = 1;
e_2 = matchValue;
}
else {
matchResult = 2;
e_3 = matchValue;
}
}
else if (matchValue instanceof Error) {
matchResult = 2;
e_3 = matchValue;
}
else {
const activePatternResult: Option<any> = isException(matchValue) ? undefined : some(matchValue);
if (activePatternResult != null) {
matchResult = 3;
value = value_1(activePatternResult);
}
else {
matchResult = 4;
e_4 = matchValue;
}
}
switch (matchResult) {
case 0:
return "TypeError: " + (e_1!).message;
case 1:
return "aborted";
case 2:
return (e_3!).name;
case 3:
return "thrown " + toStringOrEmpty(value!);
default:
return exceptionMessage(e_4!);
}
}
}
On .NET every thrown value is an exception, so JS.Thrown never matches there. An F# async turns a thrown value that is not an error into an exception, so it does not match inside one either.
e.Message of a thrown value
When you catch a thrown value as exn and read e.Message, Fickl gives String(e) for a value that is not an object. After throw "boom", e.Message is "boom", and after throw undefined it is "undefined", instead of undefined or a TypeError. In the example above, the last case compiles to exceptionMessage(e_4).
[<JsError>]
By default an F# exception compiles to a plain object, not an Error, so it has no stack and error reporters do not print it as an error. Mark an exception [<JsError>] and it extends a native Error subclass, with a stack captured when it is created.
open Fickl.Core
[<JsError>]
exception Timeout of milliseconds: int
let wait () =
try
raise (Timeout 500)
with Timeout ms ->
ms
import { FSharpError } from "@fable-org/fable-library-js/Types.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";
export class Timeout extends FSharpError {
constructor(milliseconds) {
super();
this.milliseconds = (milliseconds | 0);
}
}
export function Timeout_$reflection() {
return class_type("Example4.Timeout", undefined, Timeout, class_type("System.Exception"));
}
export function wait() {
try {
throw new Timeout(500);
}
catch (matchValue) {
if (matchValue instanceof Timeout) {
return matchValue.milliseconds | 0;
}
else {
throw matchValue;
}
}
}
import { FSharpError } from "@fable-org/fable-library-ts/Types.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
export class Timeout extends FSharpError {
readonly milliseconds: int32;
constructor(milliseconds: int32) {
super();
this.milliseconds = (milliseconds | 0);
}
}
export function Timeout_$reflection(): TypeInfo {
return class_type("Example4.Timeout", undefined, Timeout, class_type("System.Exception"));
}
export function wait(): int32 {
try {
throw new Timeout(500);
}
catch (matchValue: any) {
if (matchValue instanceof Timeout) {
const ms: int32 = matchValue.milliseconds | 0;
return ms | 0;
}
else {
throw matchValue;
}
}
}
Timeout compiles to class Timeout extends FSharpError. It still matches, compares and prints as an F# exception, and its inner exception is also its cause. Only this exception pays for the stack capture.
--nativeErrors
--nativeErrors does what [<JsError>] does for every exception, from failwith, invalidArg and the runtime library to a declared F# exception. Each is instanceof Error, has a stack, and has its inner exception as its cause, so Node.js, test runners and error reporters print it as an error.
exception Invalid of field: string
let parse (text: string) =
if text = "" then raise (Invalid "text")
try
int text
with e ->
raise (System.FormatException("not a number", e))
import { FSharpException } from "@fable-org/fable-library-js/Types.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";
import { FormatException_$ctor_68CE3CA2 } from "@fable-org/fable-library-js/System.js";
import { parse as parse_1 } from "@fable-org/fable-library-js/Int32.js";
import { enableNativeErrors } from "@fable-org/fable-library-js/Util.js";
enableNativeErrors();
export class Invalid extends FSharpException {
constructor(field) {
super();
this.field = field;
}
}
export function Invalid_$reflection() {
return class_type("Example5.Invalid", undefined, Invalid, class_type("System.Exception"));
}
export function parse(text) {
if (text === "") {
throw new Invalid("text");
}
try {
return parse_1(text, 66047, false, 32) | 0;
}
catch (e) {
throw FormatException_$ctor_68CE3CA2("not a number", e);
}
}
import { FSharpException } from "@fable-org/fable-library-ts/Types.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { FormatException_$ctor_68CE3CA2 } from "@fable-org/fable-library-ts/System.ts";
import { type int32, parse as parse_1 } from "@fable-org/fable-library-ts/Int32.ts";
import { enableNativeErrors } from "@fable-org/fable-library-ts/Util.ts";
enableNativeErrors();
export class Invalid extends FSharpException {
readonly field: string;
constructor(field: string) {
super();
this.field = field;
}
}
export function Invalid_$reflection(): TypeInfo {
return class_type("Example5.Invalid", undefined, Invalid, class_type("System.Exception"));
}
export function parse(text: string): int32 {
if (text === "") {
throw new Invalid("text");
}
try {
return parse_1(text, 66047, false, 32) | 0;
}
catch (e: any) {
throw FormatException_$ctor_68CE3CA2("not a number", e);
}
}
The compiled module calls enableNativeErrors() from the runtime library when it loads. The flag affects the whole program, including libraries compiled without it.
--checkedDivision
On .NET, dividing an integer or a decimal by zero raises DivideByZeroException. In JavaScript without the flag, a 32-bit or smaller integer division by zero gives 0, and int64, bigint and decimal throw an error of the runtime library that :? DivideByZeroException does not catch. --checkedDivision checks the divisor of /, % and Math.DivRem and raises DivideByZeroException when it is zero.
let average (total: int) (count: int) =
try
total / count
with :? System.DivideByZeroException ->
0
import { DivideByZeroException } from "@fable-org/fable-library-js/System.js";
import { nonZero } from "@fable-org/fable-library-js/Int32.js";
export function average(total, count) {
try {
return ~~(total / nonZero(count)) | 0;
}
catch (matchValue) {
if (matchValue instanceof DivideByZeroException) {
return 0;
}
else {
throw matchValue;
}
}
}
import { DivideByZeroException } from "@fable-org/fable-library-ts/System.ts";
import { type int32, nonZero } from "@fable-org/fable-library-ts/Int32.ts";
export function average(total: int32, count: int32): int32 {
try {
return ~~(total / nonZero(count)) | 0;
}
catch (matchValue: any) {
if (matchValue instanceof DivideByZeroException) {
return 0;
}
else {
throw matchValue;
}
}
}
With the flag, total / count compiles to ~~(total / nonZero(count)) | 0, where nonZero raises the exception. A divisor that is a constant other than zero is not checked.
See Compiler flags for the other flags.