Skip to content

Async functions

This page covers the jsAsync { } and promise { } computation expressions, which compile to native async functions, and the conversions between an F# CancellationToken and the web AbortSignal. You need them when your F# code awaits JavaScript promises, or when it passes cancellation to fetch and other cancellable JavaScript APIs.

jsAsync and promise

jsAsync { } compiles to a native async function that returns a JS.Promise. Each let! is an await, and return resolves the promise. promise { } is the same builder under the name Fable.Promise uses, so existing promise { } code compiles to native async functions too. Both live in Fickl.Core.

open Fable.Core
open Fickl.Core

let addLater (first: JS.Promise<int>) (second: JS.Promise<int>) = jsAsync {
    let! a = first
    let! b = second
    return a + b
}
export function addLater(first, second) {
    return (async () => (((await first) + (await second)) | 0))();
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export function addLater(first: Promise<int32>, second: Promise<int32>): Promise<int32> {
    return (async (): Promise<int32> => (((await first) + (await second)) | 0))();
}

The body is an async arrow function that is called at once, so the work starts running when you call addLater, not when something awaits the result. There is no F# cancellation token in a jsAsync body: pass one to the work yourself, as described in Cancellation.

try ... with catches a rejected promise as well as a thrown exception, try ... finally runs after the promise settles, and use disposes its resource after the body settles.

Awaiting several promises with and!

let! ... and! ... awaits its promises together with Promise.all instead of one after the other. You can join up to six promises this way.

open Fable.Core
open Fickl.Core

let describe (name: JS.Promise<string>) (count: JS.Promise<int>) = promise {
    let! n = name
    and! c = count
    return $"{n}: {c}"
}
export function describe(name, count) {
    return (async () => {
        const _arg = await Promise.all([name, count]);
        return `${_arg[0]}: ${_arg[1]}`;
    })();
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export function describe(name: Promise<string>, count: Promise<int32>): Promise<string> {
    return (async (): Promise<string> => {
        const _arg: [string, int32] = await Promise.all([name, count] as [Promise<string>, Promise<int32>]);
        return `${_arg[0]}: ${_arg[1]}`;
    })();
}

The promises are passed to Promise.all as they are, so they run at the same time only if they were started before the let!. A promise that rejects rejects the whole computation.

for over a promise and over an async iterable

for value in source do has two forms besides the ordinary loop over a sequence. Over a JS.AsyncIterable<'T> it compiles to for await, and leaving the loop early closes the iterator. Over a single JS.Promise<'T> it awaits the promise and runs the body once with its value.

open Fable.Core
open Fickl.Core

let total (values: JS.AsyncIterable<int>) = jsAsync {
    let mutable sum = 0
    for value in values do
        sum <- sum + value
    return sum
}

let double (answer: JS.Promise<int>) = jsAsync {
    for value in answer do
        return value * 2
}
export function total(values) {
    return (async () => {
        let sum = 0;
        for await (const _arg of values) {
            sum = ((sum + _arg) | 0);
        }
        return sum | 0;
    })();
}

export function double(answer) {
    return (async () => (((await answer) * 2) | 0))();
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export function total(values: AsyncIterable<int32>): Promise<int32> {
    return (async (): Promise<int32> => {
        let sum = 0;
        for await (const _arg of values) {
            sum = ((sum + _arg) | 0);
        }
        return sum | 0;
    })();
}

export function double(answer: Promise<int32>): Promise<int32> {
    return (async (): Promise<int32> => (((await answer) * 2) | 0))();
}

A JsAsyncGenerator is a JS.AsyncIterable, so you can loop over the async generators from Generators the same way.

[<AsyncFunction>]

Without the attribute, a function returning jsAsync is an ordinary function that calls an async arrow function. Put [<AsyncFunction>] on a module function returning jsAsync and the function itself is the async function.

open Fable.Core
open Fickl.Core

[<AsyncFunction>]
let fetchTwice (promise: JS.Promise<int>) = jsAsync {
    let! first = promise
    let! second = promise
    return first + second
}
export const fetchTwice = async function (promise) {
    return ((await promise) + (await promise)) | 0;
};
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export const fetchTwice = async function (this: any, promise: Promise<int32>): Promise<int32> {
    return ((await promise) + (await promise)) | 0;
};

The compiled value is a const bound to an async function expression, so it has its own this, which TypeScript types as this: any. The attribute applies to module functions only.

Cancellation

Fickl.Core adds two extension members that convert between a CancellationToken and an AbortSignal. token.ToAbortSignal() is a signal that aborts when the token is cancelled, ready to pass to fetch. CancellationToken.FromAbortSignal(signal) is a token that is cancelled when the signal aborts, for example the signal of an incoming request.

open System.Threading
open Fable.Core
open Fickl.Core

[<Global>]
let fetch (url: string) (init: obj) : JS.Promise<obj> = jsNative

let load (token: CancellationToken) =
    fetch "/data" {| signal = token.ToAbortSignal() |}

let handle (request: {| signal: JS.AbortSignal |}) =
    let token = CancellationToken.FromAbortSignal(request.signal)
    token.IsCancellationRequested
import { fromAbortSignal, toAbortSignal } from "@fable-org/fable-library-js/Cancellation.js";
import { isCancellationRequested } from "@fable-org/fable-library-js/Async.js";

export function load(token) {
    return fetch("/data", {
        signal: toAbortSignal(token),
    });
}

export function handle(request) {
    return isCancellationRequested(fromAbortSignal(request.signal));
}
import { fromAbortSignal, toAbortSignal } from "@fable-org/fable-library-ts/Cancellation.ts";
import type { CancellationToken } from "@fable-org/fable-library-ts/AsyncBuilder.ts";
import { isCancellationRequested } from "@fable-org/fable-library-ts/Async.ts";

export function load(token: CancellationToken): Promise<any> {
    return fetch("/data", {
        signal: toAbortSignal(token),
    });
}

export function handle(request: { signal: AbortSignal }): boolean {
    return isCancellationRequested(fromAbortSignal(request.signal));
}

The same token always gives the same signal, and the same signal the same token. A signal from ToAbortSignal gives its token back, and the token from FromAbortSignal gives its signal back. JS.AbortSignal and JS.AbortController bind the web types.

F# async and promises

Two overloads connect an F# async to promises under cancellation. Async.AwaitPromise(promise, token) stops waiting when the token is cancelled, and the computation is then cancelled without waiting for the promise. Async.StartAsPromise(work, signal) starts the computation as a promise and cancels it when the signal aborts.

open System
open System.Threading
open Fable.Core
open Fickl.Core

let waitFor (token: CancellationToken) (work: JS.Promise<int>) = async {
    try
        let! value = Async.AwaitPromise(work, token)
        return Some value
    with :? OperationCanceledException ->
        return None
}

let start (signal: JS.AbortSignal) =
    Async.StartAsPromise(Async.Sleep 1000, signal)
import { OperationCanceledException, singleton } from "@fable-org/fable-library-js/AsyncBuilder.js";
import { fromAbortSignal, awaitPromiseWithToken } from "@fable-org/fable-library-js/Cancellation.js";
import { defaultOf } from "@fable-org/fable-library-js/Util.js";
import { sleep, startAsPromise } from "@fable-org/fable-library-js/Async.js";

export function waitFor(token, work) {
    return singleton.Delay(() => singleton.TryWith(singleton.Delay(() => singleton.Bind(awaitPromiseWithToken(work, token), (_arg) => singleton.Return(_arg))), (_arg_1) => {
        if (_arg_1 instanceof OperationCanceledException) {
            return singleton.Return(undefined);
        }
        else {
            throw _arg_1;
            return defaultOf();
        }
    }));
}

export function start(signal) {
    return startAsPromise(sleep(1000), fromAbortSignal(signal));
}
import { type CancellationToken, OperationCanceledException, type Async, singleton } from "@fable-org/fable-library-ts/AsyncBuilder.ts";
import { fromAbortSignal, awaitPromiseWithToken } from "@fable-org/fable-library-ts/Cancellation.ts";
import type { Option } from "@fable-org/fable-library-ts/Option.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { type Exception, defaultOf } from "@fable-org/fable-library-ts/Util.ts";
import { sleep, startAsPromise } from "@fable-org/fable-library-ts/Async.ts";

export function waitFor(token: CancellationToken, work: Promise<int32>): Async<Option<int32>> {
    return singleton.Delay<Option<int32>>((): Async<Option<int32>> => singleton.TryWith<Option<int32>>(singleton.Delay<Option<int32>>((): Async<Option<int32>> => singleton.Bind<int32, Option<int32>>(awaitPromiseWithToken(work, token), (_arg: int32): Async<Option<int32>> => singleton.Return<Option<int32>>(_arg))), (_arg_1: Exception): Async<Option<int32>> => {
        if (_arg_1 instanceof OperationCanceledException) {
            return singleton.Return<Option<int32>>(undefined);
        }
        else {
            throw _arg_1;
            return defaultOf();
        }
    }));
}

export function start(signal: AbortSignal): Promise<void> {
    return startAsPromise(sleep(1000), fromAbortSignal(signal));
}

AwaitPromise with a token stops only the wait. To stop the work too, pass the token's ToAbortSignal() to the code that made the promise.

AbortError

An aborted fetch rejects with a DOMException named "AbortError", and cancelling a token aborts its signal with the same error. Fickl treats the two sides as one:

  • An AbortError is an OperationCanceledException for :?, so :? OperationCanceledException catches an aborted fetch.
  • An F# async awaiting a promise that rejects with an AbortError is cancelled, not failed.
  • An OperationCanceledException is named "AbortError", so JavaScript code that checks e.name === "AbortError" sees F# cancellation as an abort. A computation started with Async.StartAsPromise(work, signal) rejects with it when the signal aborts.

JS.awaitTopLevel

JS.awaitTopLevel awaits a promise at the top level of a module, so a module-level value has the resolved type instead of a Promise.

open Fable.Core
open Fickl.Core

type Config = { url: string }

[<Global>]
let loadConfig () : JS.Promise<Config> = jsNative

let config: Config = JS.awaitTopLevel (loadConfig ())
import { Record } from "@fable-org/fable-library-js/Types.js";
import { record_type, string_type } from "@fable-org/fable-library-js/Reflection.js";

export class Config extends Record {
    constructor(url) {
        super();
        this.url = url;
    }
}

export function Config_$reflection() {
    return record_type("Example7.Config", [], Config, () => [["url", string_type]]);
}

export const config = await loadConfig();
import { Record } from "@fable-org/fable-library-ts/Types.ts";
import type { IComparable, IEquatable } from "@fable-org/fable-library-ts/Util.ts";
import { record_type, string_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Config extends Record implements IEquatable<Config>, IComparable<Config> {
    readonly url: string;
    constructor(url: string) {
        super();
        this.url = url;
    }
}

export function Config_$reflection(): TypeInfo {
    return record_type("Example7.Config", [], Config, () => [["url", string_type]]);
}

export const config: Config = await loadConfig();

This compiles to export const config = await loadConfig();. It is allowed only as the whole initializer of a module-level value; anywhere else it is a compile error. On .NET it throws.

TypeScript needs module set to es2022, esnext, preserve or nodenext, and a target of es2017 or later.

Edit this page