Skip to content

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.

Edit this page