O Result Pattern no Node.js representa sucesso ou falha como um valor explícito, em vez de usar exceções para todo resultado negativo. Uma função retorna Ok com o valor ou Err com um erro tipado, obrigando o chamador a tratar os dois caminhos.
Esse padrão é útil para validação, regras de negócio, parsing e integrações em que a falha é esperada. Um e-mail inválido, estoque insuficiente ou conflito de versão não precisa percorrer a aplicação como exceção inesperada.
Neste guia, você aprenderá a implementar Result em TypeScript, criar tipos de erro, compor operações, integrar com async, controllers, Value Objects, repositories, testes e observabilidade.
O que é Result Pattern?
Result é um tipo discriminado com duas possibilidades:
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };A ideia é comum em linguagens como Rust e em programação funcional. No TypeScript, discriminated unions permitem narrowing seguro. A documentação oficial explica discriminated unions.
Para tipos que validam na criação, consulte Value Objects no Node.js.
Helpers básicos
export function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
export function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}O uso fica simples:
const result = Email.parse(input.email);
if (!result.ok) {
return handleEmailError(result.error);
}
const email = result.value;Exceções versus Result
Use Result quando a falha faz parte do contrato normal:
- entrada inválida;
- registro não encontrado esperado;
- conflito de negócio;
- estoque insuficiente;
- limite excedido;
- parsing de dado externo.
Use exceção para falhas inesperadas:
- bug de programação;
- invariante interna quebrada;
- corrupção de estado;
- erro de infraestrutura que não foi modelado;
- configuração obrigatória ausente.
Erro tipado
type CreateUserError =
| { type: 'invalid_email'; input: string }
| { type: 'email_in_use'; email: string }
| { type: 'weak_password'; reasons: string[] };O chamador conhece todos os casos relevantes.
Caso de uso
async function createUser(
input: CreateUserInput
): Promise<Result<UserOutput, CreateUserError>> {
const emailResult = Email.parse(input.email);
if (!emailResult.ok) {
return err({
type: 'invalid_email',
input: input.email
});
}
const exists = await users.existsByEmail(
emailResult.value
);
if (exists) {
return err({
type: 'email_in_use',
email: emailResult.value.value
});
}
const passwordResult = Password.parse(input.password);
if (!passwordResult.ok) {
return err({
type: 'weak_password',
reasons: passwordResult.error.reasons
});
}
const user = User.create({
email: emailResult.value,
password: passwordResult.value
});
await users.add(user);
return ok(UserPresenter.toOutput(user));
}Exhaustive switch
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`);
}
function mapError(error: CreateUserError) {
switch (error.type) {
case 'invalid_email':
return { status: 400, code: 'INVALID_EMAIL' };
case 'email_in_use':
return { status: 409, code: 'EMAIL_IN_USE' };
case 'weak_password':
return { status: 422, code: 'WEAK_PASSWORD' };
default:
return assertNever(error);
}
}Ao adicionar um novo erro, o compilador aponta switches incompletos.
Controller HTTP
const result = await createUser.execute(req.body);
if (!result.ok) {
const mapped = mapError(result.error);
return res.status(mapped.status).json({
code: mapped.code
});
}
return res.status(201).json(result.value);O caso de uso não conhece status HTTP.
Result em Value Objects
class Quantity {
private constructor(readonly value: number) {}
static parse(value: number): Result<Quantity, QuantityError> {
if (!Number.isSafeInteger(value)) {
return err({ type: 'not_integer' });
}
if (value <= 0) {
return err({ type: 'not_positive' });
}
return ok(new Quantity(value));
}
}Entradas externas podem ser tratadas sem try/catch.
Map
map transforma o valor de sucesso:
function map<T, U, E>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> {
return result.ok
? ok(fn(result.value))
: result;
}Exemplo:
const normalized = map(
Email.parse(raw),
email => email.value
);MapError
function mapError<T, E, F>(
result: Result<T, E>,
fn: (error: E) => F
): Result<T, F> {
return result.ok
? result
: err(fn(result.error));
}Isso traduz erros de uma camada para outra.
FlatMap
function flatMap<T, U, E, F>(
result: Result<T, E>,
fn: (value: T) => Result<U, F>
): Result<U, E | F> {
return result.ok
? fn(result.value)
: result;
}Permite encadear validações dependentes.
Composição
const email = Email.parse(input.email);
const activeEmail = flatMap(
email,
value => ensureAllowedDomain(value)
);Para muitos passos, código imperativo com if (!result.ok) pode ser mais legível.
Combine
Validações independentes podem acumular erros:
type ValidationResult<T> = Result<T, ValidationError[]>;Em formulários, retornar todos os campos inválidos melhora a experiência. Em fluxos dependentes, falhar cedo costuma ser adequado.
Async Result
type AsyncResult<T, E> = Promise<Result<T, E>>;Um repository pode retornar:
findById(id: OrderId): AsyncResult<Order, RepositoryError>;Porém, usar Result em toda infraestrutura pode gerar verbosidade. Escolha fronteiras claras.
Capturando Promise
async function fromPromise<T, E>(
promise: Promise<T>,
map: (error: unknown) => E
): AsyncResult<T, E> {
try {
return ok(await promise);
} catch (error) {
return err(map(error));
}
}Converta apenas erros que você sabe classificar.
Não engula bugs
catch (error) {
return err({ type: 'unknown' });
}Esse padrão pode esconder TypeError e bugs. Verifique tipos, códigos e origem antes de converter.
Erros de repository
type RepositoryError =
| { type: 'unavailable'; retryable: true }
| { type: 'conflict'; retryable: false }
| { type: 'timeout'; retryable: true };O caso de uso pode traduzir para erro de aplicação sem expor SQLSTATE.
Not Found
Há três opções comuns:
Promise<Order | null>
Result<Order, NotFoundError>
Option<Order>Use null quando ausência é simples e esperada. Use Result quando precisa carregar contexto ou diferenciar causas.
Result e Repository Pattern
Repositories podem lançar erros inesperados e retornar null para ausência, enquanto o Service Layer converte conflitos conhecidos. Consulte Repository Pattern no Node.js.
Result e Service Layer
O caso de uso é uma boa fronteira para retornar erros de negócio tipados. Consulte Service Layer no Node.js.
Result e transações
Uma Unit of Work deve reverter quando o callback retorna Err:
const result = await callback(tx);
if (!result.ok) {
await client.query('ROLLBACK');
return result;
}
await client.query('COMMIT');
return result;Ou o callback pode lançar apenas para sinalizar rollback. Defina uma convenção única.
Consulte Unit of Work no Node.js.
Adapter de fila
const result = await processMessage.execute(message);
if (!result.ok) {
switch (result.error.type) {
case 'invalid_message':
return deadLetter(message, result.error);
case 'temporary_unavailable':
throw new RetryableMessageError();
}
}O adapter decide retry, ack ou dead-letter.
GraphQL
Uma mutation pode retornar union de sucesso e erros esperados:
union CreateUserResult =
UserCreated |
InvalidEmailError |
EmailInUseErrorO resolver converte Result para o tipo GraphQL.
Logs
Erros esperados não precisam de stack em nível error:
- validação: debug ou info;
- conflito: info;
- dependência temporária: warn;
- bug inesperado: error.
Isso reduz ruído.
Métricas
Conte resultados por tipo de baixa cardinalidade:
application_results_total{
operation="create_user",
result="email_in_use"
}Não use mensagens livres como label.
Bibliotecas
Existem bibliotecas como neverthrow, que fornecem Result, ResultAsync e métodos de composição. Avalie manutenção, bundle, ergonomia e compatibilidade antes de adotar.
Implementação própria
Um union simples pode ser suficiente. Evite construir uma biblioteca funcional complexa se a equipe só precisa de Ok e Err.
Unwrap
function unwrap<T, E>(result: Result<T, E>): T {
if (!result.ok) {
throw new Error('Tried to unwrap Err');
}
return result.value;
}Use apenas em testes ou pontos em que Err é impossível por contrato. Em produção, unwrap indiscriminado devolve exceções ao fluxo.
UnwrapOr
function unwrapOr<T, E>(
result: Result<T, E>,
fallback: T
): T {
return result.ok ? result.value : fallback;
}Use quando fallback é realmente seguro.
Tap
function tap<T, E>(
result: Result<T, E>,
fn: (value: T) => void
): Result<T, E> {
if (result.ok) fn(result.value);
return result;
}Evite efeitos externos em cadeias que podem ser repetidas.
Nomes de erros
Prefira tipos semânticos:
{ type: 'insufficient_stock', available: 2 }
Evite:
{ type: 'error', message: 'failed' }Dados sensíveis
Erros retornados ao cliente não devem carregar stack, SQL, token ou detalhes de infraestrutura. O adapter cria uma resposta segura.
Versionamento
Quando erros fazem parte de contrato público, adicionar ou remover variantes pode afetar clientes. Documente códigos em OpenAPI ou GraphQL.
Consulte OpenAPI com Node.js.
Testes
test('retorna email_in_use', async () => {
const users = new InMemoryUserRepository([
userWithEmail('user@example.com')
]);
const result = await createUser(users).execute({
email: 'user@example.com',
password: 'SafePassword123!'
});
assert.equal(result.ok, false);
if (!result.ok) {
assert.equal(result.error.type, 'email_in_use');
}
});Teste de exaustividade
O TypeScript verifica em compilação, mas testes confirmam mapeamento HTTP e mensagens públicas para cada variante.
Teste de infraestrutura
Simule timeout, conflito e indisponibilidade. Confirme que apenas erros conhecidos viram Result e bugs continuam visíveis.
Property-based testing
Gere entradas aleatórias para validar que parser nunca lança e sempre retorna Ok ou Err dentro do contrato.
Erros comuns
- Result para todo bug: falhas inesperadas são escondidas.
- Exceção dentro de Err: contrato continua opaco.
- Erro genérico: chamador não sabe decidir.
- Unwrap em produção: exceções reaparecem.
- Muitas combinações funcionais: leitura fica difícil.
- Sem mapeamento por camada: SQLSTATE vaza para HTTP.
- Result ignorado: falha passa silenciosamente.
Boas práticas
- Use unions discriminadas.
- Modele falhas esperadas.
- Mantenha bugs como exceções.
- Faça switches exaustivos.
- Mapeie erros nas fronteiras.
- Use Result em Value Objects.
- Não esconda infraestrutura desconhecida.
- Registre erros por severidade.
- Documente códigos públicos.
- Teste todos os caminhos.
Conclusão
O Result Pattern no Node.js transforma falhas esperadas em parte explícita do tipo de retorno. Ok e Err tornam validações, conflitos e regras de negócio visíveis para o chamador.
O padrão funciona melhor quando erros são semânticos, switches são exaustivos e exceções continuam reservadas para falhas inesperadas. Com mapeamento nas bordas, Value Objects e testes, Result reduz try/catch genérico sem esconder bugs.




