ESLint Plugin for NeverThrow
The eslint-plugin-neverthrow ensures that Result and ResultAsync values are properly handled in your codebase, preventing unhandled errors from slipping through.
Overview
Created by mdbetancourt as part of NeverThrow’s bounty program, this ESLint plugin enforces that errors are not left unhandled.
This plugin is essentially a porting of Rust’s must-use attribute to TypeScript/JavaScript.
Installation
Install the plugin via npm:
npm install eslint-plugin-neverthrow --save-dev
Or using other package managers:
# Yarn
yarn add -D eslint-plugin-neverthrow
# pnpm
pnpm add -D eslint-plugin-neverthrow
Configuration
Add the plugin to your ESLint configuration file:
Using Flat Config (ESLint 9+)
// eslint.config.js
import neverthrow from 'eslint-plugin-neverthrow'
export default [
{
plugins: {
neverthrow
},
rules: {
'neverthrow/must-use-result': 'error'
}
}
]
Using Legacy Config (.eslintrc)
{
"plugins": ["neverthrow"],
"rules": {
"neverthrow/must-use-result": "error"
}
}
Rules
must-use-result
This rule ensures that Result and ResultAsync values are consumed in one of the following ways:
- Calling
.match()
- Calling
.unwrapOr()
- Calling
._unsafeUnwrap() (for test environments)
This guarantees that you’re explicitly handling the error case of your Result.
Valid Patterns
The following patterns are considered valid:
Using match
import { Result } from 'neverthrow'
function getUser(id: string): Result<User, Error> {
// ...
}
// Valid: Using match to handle both cases
getUser('123').match(
(user) => console.log('Success:', user),
(error) => console.error('Error:', error)
)
Using unwrapOr
import { Result } from 'neverthrow'
function parseNumber(str: string): Result<number, string> {
// ...
}
// Valid: Providing a default value
const num = parseNumber('42').unwrapOr(0)
Using _unsafeUnwrap in Tests
import { expect, test } from 'vitest'
import { parseUser } from './parser'
test('parses valid user', () => {
const result = parseUser({ id: 1, name: 'Alice' })
// Valid: Unsafe unwrap in test environment
expect(result._unsafeUnwrap()).toEqual({ id: 1, name: 'Alice' })
})
Chaining Operations
// Valid: Final result is consumed
getUser('123')
.map(user => user.name)
.mapErr(error => error.message)
.match(
(name) => console.log(name),
(msg) => console.error(msg)
)
Invalid Patterns
The following patterns will trigger ESLint errors:
Not Consuming the Result
import { Result } from 'neverthrow'
function getUser(id: string): Result<User, Error> {
// ...
}
// ❌ Invalid: Result is not consumed
getUser('123')
// ❌ Invalid: Mapped but final result not consumed
getUser('123').map(user => user.name)
Only Handling Success Case
// ❌ Invalid: Only handles ok case with map
getUser('123').map(user => {
console.log('User:', user)
})
// Must use .match() or .unwrapOr() to explicitly handle errors
Ignoring Async Results
import { ResultAsync } from 'neverthrow'
function fetchUser(id: string): ResultAsync<User, Error> {
// ...
}
// ❌ Invalid: ResultAsync not consumed
fetchUser('123')
// ❌ Invalid: Awaited but not consumed
await fetchUser('123')
Fixing Violations
When the plugin reports a violation, you have three options to fix it:
Option 1: Use match
Handle both success and error cases explicitly:
getUser('123').match(
(user) => handleSuccess(user),
(error) => handleError(error)
)
Option 2: Use unwrapOr
Provide a default value for the error case:
const user = getUser('123').unwrapOr(null)
if (user) {
// Use user
}
Option 3: Use _unsafeUnwrap (Tests Only)
For test environments where you’re confident about the result:
test('should return user', () => {
const user = getUser('123')._unsafeUnwrap()
expect(user.id).toBe('123')
})
_unsafeUnwrap() should only be used in test environments. It will throw if the Result is an Err.
Integration with Type Checking
The plugin works alongside TypeScript’s type system to provide comprehensive safety:
import { Result, ok, err } from 'neverthrow'
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err('Division by zero')
}
return ok(a / b)
}
// TypeScript ensures correct types
const result = divide(10, 2)
.map(x => x * 2) // TypeScript knows x is number
.mapErr(e => `Error: ${e}`) // TypeScript knows e is string
.match(
(value) => value, // value: number
(error) => 0 // error: string
)
Working with Async Code
The plugin also validates ResultAsync handling:
import { ResultAsync } from 'neverthrow'
async function processData(id: string): ResultAsync<Data, Error> {
// ...
}
// ✅ Valid: Properly consumed
await processData('123').match(
(data) => console.log(data),
(error) => console.error(error)
)
// ✅ Valid: Chained and consumed
await processData('123')
.andThen(transformData)
.map(formatData)
.match(
(result) => result,
(error) => null
)
Configuration Options
The plugin can be configured with different severity levels:
{
"rules": {
"neverthrow/must-use-result": "error" // Error on violation
}
}
{
"rules": {
"neverthrow/must-use-result": "warn" // Warning only
}
}
{
"rules": {
"neverthrow/must-use-result": "off" // Disabled
}
}
Best Practices
Enable in All Environments
Enable the plugin in all environments except tests, where you might legitimately use _unsafeUnwrap:
// eslint.config.js
export default [
{
files: ['src/**/*.ts'],
rules: {
'neverthrow/must-use-result': 'error'
}
},
{
files: ['**/*.test.ts', '**/*.spec.ts'],
rules: {
'neverthrow/must-use-result': 'warn' // More lenient in tests
}
}
]
Combine with TypeScript Strict Mode
For maximum type safety, use the plugin alongside TypeScript’s strict mode:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true
}
}
Use with Pre-commit Hooks
Integrate the plugin with pre-commit hooks to catch issues early:
// package.json
{
"scripts": {
"lint": "eslint src/",
"precommit": "npm run lint"
}
}
External Resources
Have questions or suggestions? Join the discussion: