Encoding
The Encoding module (from the core effect package) does pure, synchronous
conversion between bytes and text in three formats: Base64 (RFC 4648),
Base64Url (the URL-safe, unpadded variant), and Hex (lowercase
hexadecimal).
There is a deliberate asymmetry between the two directions:
- Encoders are total.
encodeBase64,encodeBase64Url, andencodeHexalways return astring— anyUint8Arrayorstringcan be encoded, so they never fail. - Decoders are safe, not throwing.
decodeBase64,decodeHex, and friends returnResult.Result<…, EncodingError>because the input might be malformed. Instead of throwing, they hand you aResultyou branch on.
The mental model for inputs and outputs:
- A
stringinput is first converted to UTF-8 bytes withTextEncoder, then encoded. AUint8Arrayinput is encoded directly. - Byte decoders (
decodeBase64,decodeBase64Url,decodeHex) return a rawUint8Array. - The
*Stringdecoders (decodeBase64String,decodeBase64UrlString,decodeHexString) decode those bytes back into UTF-8 text withTextDecoder.
Round-trip example
Section titled “Round-trip example”A full encode/decode round trip. Encoding produces a plain string; decoding
produces a Result that you resolve with Result.match.
import { Encoding, Result } from "effect"
// Some binary payloadconst bytes = new Uint8Array([72, 101, 108, 108, 111]) // "Hello"
// Encode (total — always a string)const encoded = Encoding.encodeBase64(bytes)// => "SGVsbG8="
// Decode (safe — a Result, because input could be malformed)const result = Encoding.decodeBase64(encoded)
const message = Result.match(result, { onFailure: (error) => `decode failed: ${error.message}`, onSuccess: (decoded) => `got ${decoded.length} bytes back`})// => "got 5 bytes back"Base64
Section titled “Base64”Standard, padded RFC 4648 Base64. This is the default choice for embedding binary data in JSON, data URLs, or anywhere a Base64 string is expected.
import { Encoding, Result } from "effect"
// Encode textEncoding.encodeBase64("hello")// => "aGVsbG8="
// Encode bytesEncoding.encodeBase64(new Uint8Array([72, 101, 108, 108, 111]))// => "SGVsbG8="
// Decode back to bytesconst bytes = Encoding.decodeBase64("SGVsbG8=")if (Result.isSuccess(bytes)) { Array.from(bytes.success) // => [72, 101, 108, 108, 111]}
// Decode straight to UTF-8 textconst text = Encoding.decodeBase64String("aGVsbG8=")if (Result.isSuccess(text)) { text.success // => "hello"}Use decodeBase64 when you want the raw Uint8Array (binary payloads), and
decodeBase64String when the encoded bytes are UTF-8 text and you want the
string directly.
Base64Url
Section titled “Base64Url”The URL-safe variant: it replaces + with - and / with _, and emits
unpadded output (no trailing =). This is the form used for JWT segments,
URL query parameters, and filenames.
import { Encoding, Result } from "effect"
// Encode text — note the trailing "_" instead of "/" and no paddingEncoding.encodeBase64Url("hello?")// => "aGVsbG8_"
// Encode bytesEncoding.encodeBase64Url(new Uint8Array([72, 101, 108, 108, 111, 63]))// => "SGVsbG8_"
// Decode back to bytes (accepts padded AND unpadded input)const bytes = Encoding.decodeBase64Url("SGVsbG8_")if (Result.isSuccess(bytes)) { Array.from(bytes.success) // => [72, 101, 108, 108, 111, 63]}
// Decode straight to UTF-8 textconst text = Encoding.decodeBase64UrlString("aGVsbG8_")if (Result.isSuccess(text)) { text.success // => "hello?"}A common use is splitting and decoding a JWT, whose three segments are Base64Url-encoded:
import { Encoding, Result } from "effect"
const decodeJwtPayload = (jwt: string) => { const segment = jwt.split(".")[1] ?? "" return Result.map(Encoding.decodeBase64UrlString(segment), JSON.parse)}Lowercase hexadecimal. Encoding always emits lowercase letters; decoding requires an even number of hex characters.
import { Encoding, Result } from "effect"
// Encode textEncoding.encodeHex("hello")// => "68656c6c6f"
// Encode bytesEncoding.encodeHex(new Uint8Array([72, 101, 108, 108, 111]))// => "48656c6c6f"
// Decode back to bytesconst bytes = Encoding.decodeHex("48656c6c6f")if (Result.isSuccess(bytes)) { Array.from(bytes.success) // => [72, 101, 108, 108, 111]}
// Decode straight to UTF-8 textconst text = Encoding.decodeHexString("68656c6c6f")if (Result.isSuccess(text)) { text.success // => "hello"}Handling decode failures
Section titled “Handling decode failures”Decoders never throw. A malformed input becomes Result.fail carrying an
EncodingError, which records what went wrong.
import { Encoding, Result } from "effect"
// "!" is not a valid Base64 characterconst result = Encoding.decodeBase64("not-base64!")
Result.match(result, { onFailure: (error) => { error._tag // => "EncodingError" error.kind // => "Decode" error.module // => "Base64" error.message // human-readable reason }, onSuccess: (bytes) => bytes})Narrowing with isEncodingError
Section titled “Narrowing with isEncodingError”When a value’s type is wider than EncodingError (e.g. caught from a mixed
error channel), use the isEncodingError guard to narrow it.
import { Encoding } from "effect"
const handle = (u: unknown) => { if (Encoding.isEncodingError(u)) { // u is now typed as EncodingError return `${u.module} ${u.kind} failed: ${u.message}` } return "unknown error"}Pulling a Result into an Effect
Section titled “Pulling a Result into an Effect”Inside Effect.gen, lift the decode Result into the Effect error channel with
Effect.fromResult. A Result.fail becomes a typed EncodingError failure you
can handle with Effect.catchTag.
import { Effect, Encoding } from "effect"
const program = Effect.gen(function* () { // Fails with EncodingError if the input is not valid Base64 const bytes = yield* Effect.fromResult(Encoding.decodeBase64("SGVsbG8=")) return bytes.length}).pipe( // EncodingError is a tagged error, so catchTag narrows it precisely Effect.catchTag("EncodingError", (error) => Effect.succeed(`bad input: ${error.message}`) ))API reference
Section titled “API reference”Every public export of the Encoding module. Encoders return string; decoders
return Result.Result<…, EncodingError>.
encodeBase64
Section titled “encodeBase64”Encodes a string (as UTF-8 bytes) or a Uint8Array into a standard padded
RFC 4648 Base64 string.
import { Encoding } from "effect"
Encoding.encodeBase64("hello")// => "aGVsbG8="Encoding.encodeBase64(new Uint8Array([255, 0, 255]))// => "/wD/"decodeBase64
Section titled “decodeBase64”Decodes a standard Base64 string into a Uint8Array, returning a Result.
Fails if the length is not a multiple of 4 or padding is misplaced.
import { Encoding, Result } from "effect"
Encoding.decodeBase64("aGVsbG8=")// => Result.succeed(Uint8Array [104, 101, 108, 108, 111])Result.isFailure(Encoding.decodeBase64("abc"))// => true (length is not a multiple of 4)decodeBase64String
Section titled “decodeBase64String”Decodes a standard Base64 string into UTF-8 text, returning a Result. Equivalent
to decodeBase64 followed by TextDecoder.decode.
import { Encoding } from "effect"
Encoding.decodeBase64String("aGVsbG8=")// => Result.succeed("hello")encodeBase64Url
Section titled “encodeBase64Url”Encodes a string or Uint8Array into URL-safe, unpadded Base64 (-/_
instead of +//, no trailing =).
import { Encoding } from "effect"
Encoding.encodeBase64Url("hello?")// => "aGVsbG8_"Encoding.encodeBase64Url(new Uint8Array([255, 255, 255]))// => "____"decodeBase64Url
Section titled “decodeBase64Url”Decodes URL-safe Base64 (padded or unpadded) into a Uint8Array, returning a
Result.
import { Encoding, Result } from "effect"
Encoding.decodeBase64Url("aGVsbG8_")// => Result.succeed(Uint8Array [104, 101, 108, 108, 111, 63])// Padded input is accepted too:Result.isSuccess(Encoding.decodeBase64Url("aGVsbG8="))// => truedecodeBase64UrlString
Section titled “decodeBase64UrlString”Decodes URL-safe Base64 into UTF-8 text, returning a Result.
import { Encoding } from "effect"
Encoding.decodeBase64UrlString("aGVsbG8_")// => Result.succeed("hello?")encodeHex
Section titled “encodeHex”Encodes a string or Uint8Array into a lowercase hexadecimal string.
import { Encoding } from "effect"
Encoding.encodeHex("hello")// => "68656c6c6f"Encoding.encodeHex(new Uint8Array([0, 15, 255]))// => "000fff"decodeHex
Section titled “decodeHex”Decodes a hexadecimal string into a Uint8Array, returning a Result. Fails on
an odd number of characters or non-hex characters.
import { Encoding, Result } from "effect"
Encoding.decodeHex("000fff")// => Result.succeed(Uint8Array [0, 15, 255])Result.isFailure(Encoding.decodeHex("abc"))// => true (odd length)decodeHexString
Section titled “decodeHexString”Decodes a hexadecimal string into UTF-8 text, returning a Result.
import { Encoding } from "effect"
Encoding.decodeHexString("68656c6c6f")// => Result.succeed("hello")EncodingError
Section titled “EncodingError”The structured error returned by decode failures. It is a
Data.TaggedError("EncodingError") carrying kind ("Decode" | "Encode"),
module (e.g. "Base64", "Base64Url", "Hex"), the original input, and a
human-readable message. Because it is a tagged error, it works with
Effect.catchTag("EncodingError", …).
import { Encoding, Result } from "effect"
const result = Encoding.decodeHex("zz")if (Result.isFailure(result)) { result.failure._tag // => "EncodingError" result.failure.kind // => "Decode" result.failure.module // => "Hex" result.failure.message // human-readable reason}isEncodingError
Section titled “isEncodingError”Type guard that narrows an unknown value to EncodingError by checking its
runtime marker.
import { Encoding } from "effect"
Encoding.isEncodingError(new Error("boom"))// => falseconst err = Encoding.decodeHex("zz")// inside a Result.failure branch, err.failure is an EncodingError:// Encoding.isEncodingError(err.failure) // => trueEncodingErrorTypeId
Section titled “EncodingErrorTypeId”The string marker stored on EncodingError values ("~effect/encoding/EncodingError")
that isEncodingError checks. You rarely reference it directly — prefer the
guard — but it is exported for advanced narrowing, along with its literal type
EncodingErrorTypeId.
import { Encoding } from "effect"
Encoding.EncodingErrorTypeId// => "~effect/encoding/EncodingError"