Skip to content

Result-Based Error Handling

Result-Based Error Handling

When a failure is expected and callers should recover from it, Result<T, E> is usually the clearest model.

Example: parse a port

import Number from 'std:number'
import Result from 'std:result'
import String from 'std:string'
type ParseError = {
tag: 'parse_error',
message: String,
}
parsePort = (text: String): Result<Number, ParseError> => {
if (String.isDigits(text)) {
Result.ok(Number.parse(text))
} else {
Result.error({
tag: 'parse_error',
message: 'port must contain digits only',
})
}
}

Example: compose with andThen

requireInRange = (value: Number): Result<Number, ParseError> => {
if (value > 0 && value < 65536) {
Result.ok(value)
} else {
Result.error({
tag: 'parse_error',
message: 'port must be between 1 and 65535',
})
}
}
parseValidPort = (text: String): Result<Number, ParseError> => {
parsePort(text)
|> Result.andThen(requireInRange)
}

Why prefer this over exceptions

  • the success and error shapes are part of the type
  • callers can handle cases with match
  • failure stays visible in signatures

Use throw for exceptional situations. Use Result when failure is part of ordinary control flow.