Safely stringifies a value to JSON without throwing. bigint and Temporal are stringified as decimal/ISO strings by default.
JSON.stringify throws on bigint and on circular objects, and returns undefined for undefined/function/symbol at the top level. This never throws — it returns a fallback and handles bigint + Temporal for you.
Syntax
import { tryStringifyJSON } from '@opentf/std'; tryStringifyJSON(value: unknown, fallback?: string, options?: { replacer?: ((key: string, value: unknown) => unknown) | (string | number)[] | null, space?: string | number, temporal?: boolean }): string | undefined
value: Value to stringify. Anyunknown.fallback: Returned when stringify would throw (circular) or returnundefined(top-levelundefined/function/symbol). Defaults toundefined.options.replacer:JSON.stringifyreplacer.bigint/Temporal is converted before your function is called. Array replacers still filter keys, butbigint/Temporal inside kept keys is stringified.options.space:JSON.stringifyspace for pretty print.options.temporal:DateTime,Durationand nativeTemporal(Instant,ZonedDateTime,PlainDate, etc.) are stringified viatoString()by default (likebigint). Passtemporal:falseto disable.Datealready works viatoJSONeven without this flag.
Returns
JSON string on success, fallback (or undefined) on failure. Never throws.
Examples
tryStringifyJSON({ a: 1 }) //=> '{"a":1}' tryStringifyJSON([1, 2, 3]) //=> '[1,2,3]' tryStringifyJSON(null) //=> 'null'
// BigInt — native throws TypeError, we stringify as "1" by default tryStringifyJSON({ n: BigInt(1) }) //=> '{"n":"1"}' tryStringifyJSON({ a: BigInt("9007199254740993") }) //=> '{"a":"9007199254740993"}' tryStringifyJSON([BigInt(1), BigInt(2)]) //=> '["1","2"]' tryStringifyJSON({ a: { b: BigInt(42) } }) //=> '{"a":{"b":"42"}}' tryStringifyJSON(BigInt(123)) //=> '"123"' // top-level bigint → string // Compare: JSON.stringify({ n: BigInt(1) }) // throws TypeError
// Circular → fallback, not throw const o: any = {}; o.self = o; tryStringifyJSON(o) //=> undefined tryStringifyJSON(o, "{}") //=> "{}" // Top-level non-serializable → fallback tryStringifyJSON(undefined) //=> undefined tryStringifyJSON(undefined, "null") //=> "null" tryStringifyJSON(() => {}, "fallback") //=> "fallback" tryStringifyJSON(Symbol("a"), "x") //=> "x"
// Native edge: undefined/function/symbol inside objects/arrays follows spec tryStringifyJSON({ a: 1, b: undefined }) //=> '{"a":1}' — omitted tryStringifyJSON([1, undefined, 3]) //=> '[1,null,3]' — null in arrays
// Pretty print tryStringifyJSON({ a: 1 }, undefined, { space: 2 }) //=> '{\n "a": 1\n}' tryStringifyJSON({ a: 1 }, undefined, { space: "\t" }) //=> '{\n\t"a": 1\n}'
// Replacer as function — bigint/temporal already converted before your fn const upper = (_k: string, v: unknown) => typeof v === "string" ? (v as string).toUpperCase() : v; tryStringifyJSON({ n: BigInt(1), s: "hi" }, undefined, { replacer: upper }) //=> '{"n":"1","s":"HI"}' const omitA = (k: string, v: unknown) => k === "a" ? undefined : v; tryStringifyJSON({ a: 1, b: 2 }, undefined, { replacer: omitA }) //=> '{"b":2}'
// Replacer as array — only listed keys kept, bigint/temporal inside still stringified tryStringifyJSON({ a: 1, b: BigInt(2), c: 3 }, undefined, { replacer: ["a", "b"] }) //=> '{"a":1,"b":"2"}'
// Temporal — default true (like bigint), no flag needed import { DateTime, Duration } from '@opentf/std'; const dt = new DateTime("2024-01-01T00:00:00.000Z"); tryStringifyJSON({ t: dt }) //=> '{"t":"2024-01-01T00:00:00.000+00:00[UTC]"}' // ISO via toString() const dur = new Duration("P1DT2H"); tryStringifyJSON({ d: dur }) //=> '{"d":"P1DT2H"}' // Date already works without flag (via toJSON), but also via temporal tryStringifyJSON({ d: new Date("2024-01-01") }) //=> '{"d":"2024-01-01T00:00:00.000Z"}' // BigInt + temporal together (both default) tryStringifyJSON({ n: BigInt(1), t: dt }) //=> '{"n":"1","t":"2024-01-01T00:00:00.000+00:00[UTC]"}' // Disable temporal if you want raw tryStringifyJSON({ t: dt }, undefined, { temporal: false }) //=> '{"t":"2024-01-01T00:00:00.000+00:00[UTC]"}' // DateTime still has toJSON, same result // Native Temporal where runtime has it (Node 26+, Deno) tryStringifyJSON({ t: Temporal.Instant.from("2024-01-01T00:00:00Z") }) //=> '{"t":"2024-01-01T00:00:00Z"}' with default
// Real-world: safe localStorage write localStorage.setItem("user", tryStringifyJSON(user, "{}")!); // Real-world: safe logging without crashing on bigint/circular/temporal function logSafe(payload: unknown) { console.log(tryStringifyJSON(payload, '"[unserializable]"', { space: 2 })); } // Real-world: API response with bigint + temporal IDs const body = tryStringifyJSON({ id: BigInt("123456789012345"), createdAt: new DateTime("2024-01-01") }); //=> '{"id":"123456789012345","createdAt":"2024-01-01T00:00:00.000+00:00[UTC]"}'
Related
tryParseJSON — parse with temporal (default)
isJSONValue — guard before stringify
isJSON — check if string is JSON
DateTime — temporal datetime
Duration — temporal duration