Types Functions Patterns
Import: import { safeFetch, jsonParse, wrapError, assertNever, safeRegex, safeDate, safeNumber, safeStructuredClone, safeBigInt } from 'shotscript/std' import type { Result, PromiseResult } from 'shotscript/std'

Types
Result<T, E> type

The canonical return type for synchronous fallible functions. A discriminated tuple — either [T, null] on success or [null, E] on failure. E defaults to Error.

type Result<T, E = Error> = [T, null] | [null, E]

function parseConfig(src: string): Result<Config> {
    return jsonParse<Config>(src)
}
PromiseResult<T, E> type

The canonical return type for async fallible functions. Shorthand for Promise<Result<T, E>>.

type PromiseResult<T, E = Error> =
    Promise<Result<T, E>>

async function queryUser(id: number): PromiseResult<User> {
    const [res, err] = await safeFetch(`/users/${${id}`)
    if (err !== null) { return [null, err] }
    return jsonParse<User>(await res.text())
}

Functions
safeFetch(url, opts?) async

Wraps globalThis.fetch. Returns [Response, null] on success, [null, Error] on network failure. HTTP error status codes are not treated as errors — check res.ok yourself.

function safeFetch(
    input: string | URL,
    init: RequestInit | null,
): PromiseResult<Response>

const [res, err] = await safeFetch(`https://api.example.com/users/${${id}`, null)
if (err !== null) { return [null, err] }
if (!res.ok) { return [null, new Error(`HTTP ${${res.status}`)] }
jsonParse<T>(str) sync

Wraps JSON.parse. Returns [T, null] or [null, Error] if parsing fails.

function jsonParse<T>(str: string): Result<T>

const [user, err] = jsonParse<User>(text)
jsonStringify(val, indent?) sync

Wraps JSON.stringify. Returns [string, null] or [null, Error] if serialization fails.

function jsonStringify(
    value: unknown,
    indent: number | null,
): Result<string>
wrapError(message, cause) sync

Adds context to a propagated error — the ShotScript equivalent of Go's fmt.Errorf("context: %w", err). Sets err.cause so the original error is inspectable.

function wrapError(message: string, cause: Error): Error

const [res, err] = await safeFetch(url, null)
if (err !== null) {
    return [null, wrapError(`fetchUser: ${${url}`, err)]
}
toResult<T>(fn) sync

Wraps any synchronous dependency call that might throw — any library that doesn't return tuples.

function toResult<T>(fn: () => T): Result<T>

const [parsed, err] = toResult(function parse(): ParsedData { return someLib.parseSync(input) })
toPromiseResult<T>(fn) async

Wraps any async dependency call that might reject — any library that returns plain Promises.

function toPromiseResult<T>(
    fn: () => Promise<T>,
): PromiseResult<T>

const [rows, err] = await toPromiseResult(function query(): Promise<Row[]> { return db.query(sql) })
safeURL(url, base?) sync

Safe URL constructor — replaces the banned new URL(str) which throws on malformed input. Returns [URL, null] on success, [null, Error] on invalid URL string.

function safeURL(url: string, base: string | null): Result<URL>

const [url, err] = safeURL(rawInput, null)
if (err !== null) { return [null, err] }
safeDecodeURIComponent(str) sync

Safe decodeURIComponent — replaces the banned global which throws on malformed sequences. Returns [decoded, null] on success, [null, Error] on invalid percent-encoding.

function safeDecodeURIComponent(str: string): Result<string>

const [decoded, err] = safeDecodeURIComponent(rawStr)
safeDecodeURI(str) sync

Safe decodeURI — replaces the banned global which throws on malformed sequences. Returns [decoded, null] on success, [null, Error] on invalid percent-encoding.

function safeDecodeURI(str: string): Result<string>

const [decoded, err] = safeDecodeURI(rawStr)
safeAtob(data) sync

Safe atob — replaces the banned global which throws on invalid base64 input. Returns [decoded, null] on success, [null, Error] on invalid input.

function safeAtob(data: string): Result<string>

const [bin, err] = safeAtob(base64Str)
safeBtoa(data) sync

Safe btoa — replaces the banned global which throws on non-Latin1 characters. Returns [encoded, null] on success, [null, Error] on invalid input.

function safeBtoa(data: string): Result<string>

const [b64, err] = safeBtoa(binaryStr)
assertNever(x) sync

Exhaustive-switch helper — call in the default branch to assert a value is never. The type system guarantees this is only reachable if an exhaustiveness check fails at runtime due to a bad cast.

function assertNever(x: never): never

switch (direction) {
  case 'left': ...; break
  default: assertNever(direction)
}
safeRegex(pattern, flags?) sync

Safe new RegExp() — replaces the banned constructor which throws on invalid patterns. Returns [RegExp, null] on success, [null, Error] on invalid pattern.

function safeRegex(pattern: string, flags: string | null): Result<RegExp>

const [re, err] = safeRegex('^foo.*', 'i')
safeDate(input) sync

Safe new Date() — replaces the banned constructor which silently produces an invalid Date. Returns [Date, null] on success, [null, Error] if the input produces an invalid date.

function safeDate(input: string | number): Result<Date>

const [d, err] = safeDate('2024-01-15')
safeNumber(str) sync

Safe number parser — fails on NaN instead of returning it silently. Returns [number, null] on success, [null, Error] if the result is NaN.

function safeNumber(str: string): Result<number>

const [n, err] = safeNumber('42')
safeStructuredClone<T>(value) sync

Safe structuredClone — replaces the banned global which throws on non-cloneable values. Returns [cloned, null] on success, [null, Error] if the value cannot be cloned.

function safeStructuredClone<T>(value: T): Result<T>

const [copy, err] = safeStructuredClone(value)
safeBigInt(str) sync

Safe BigInt() — replaces the banned global which throws on invalid input. Returns [bigint, null] on success, [null, Error] on invalid input.

function safeBigInt(str: string): Result<bigint>

const [n, err] = safeBigInt('12345678901234567890')

Patterns
External library facade

When a library used in multiple places doesn't return Result/PromiseResult tuples, create a dedicated facade module. Wrap all its calls there, export Result/PromiseResult-returning functions, and have consumers import only from the facade. toResult/toPromiseResult stays confined to that one file.

// lib/db.ts — facade for the db library
import { db } from 'some-db'
import { toPromiseResult } from 'shotscript/std'
import type { PromiseResult } from 'shotscript/std'

export function dbFind(id: string): PromiseResult<Row> {
    return toPromiseResult(function find(): Promise<Row> { return db.find(id) })
}

export function dbInsert(row: Row): PromiseResult<void> {
    return toPromiseResult(function insert(): Promise<void> { return db.insert(row) })
}

// consumers import from the facade — never from the library directly
import { dbFind } from './lib/db'
const [row, err] = await dbFind(userId)