Express Async Error Handling: express-async-errors vs express-async-handler

A comprehensive comparison with code examples and practical use cases

The Async Error Problem in Express

Express.js is powerful, but it has one significant drawback: it doesn't handle errors in asynchronous route handlers by default.

The Problem Illustrated

Consider this async route handler:

// This will fail silently if getUserData rejects
app.get('/users/:id', async (req, res) => {
  const userData = await getUserData(req.params.id);
  res.json(userData);
});

If getUserData rejects with an error, Express won't catch it! The request will hang until it times out, leading to poor user experience and potential memory leaks.

Traditional Solution: try/catch Everywhere

// The traditional but verbose solution
app.get('/users/:id', async (req, res, next) => {
  try {
    const userData = await getUserData(req.params.id);
    res.json(userData);
  } catch (error) {
    next(error); // Pass to Express error handler
  }
});

This works but quickly becomes tedious when you have dozens or hundreds of routes. It also makes your code less readable by cluttering the business logic with error handling.

The Safety Net Analogy

Think of Express route handlers as acrobats performing on a high wire. Without proper error handling, there's no safety net below. If they fall (an error occurs), it's catastrophic.

The traditional try/catch approach is like making each acrobat carry their own personal safety net. It works, but it's cumbersome and distracting.

What we need instead is a single, reliable safety net that works for all acrobats automatically. That's what express-async-errors and express-async-handler provide — just in different ways.

Comparing the Two Solutions

Feature express-async-errors express-async-handler
Installation One-time global setup Applied to individual handlers
Code Modification Minimal (one import) Requires wrapping each handler
Explicitness Implicit (magic) Explicit (clear intentions)
Implementation Monkey-patches Express Higher-order function wrapper
Control All-or-nothing approach Granular control per route
NPM Weekly Downloads ~750,000 ~1,200,000

express-async-errors: The Global Solution

Installation and Setup

npm install express-async-errors

Add this import as early as possible in your application:

// app.js or index.js - at the very top
require('express');
require('express-async-errors');

// The rest of your Express setup
const app = express();
// ...

Usage Example

After the one-time setup, you can write async route handlers without try/catch:

// userRoutes.js
router.get('/:id', async (req, res) => {
  // This will properly propagate errors to Express error handlers
  const user = await User.findById(req.params.id);
  
  if (!user) {
    throw new NotFoundError('User not found');
  }
  
  res.json(user);
});

router.post('/', async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json(user);
});

Any errors thrown or promises rejected will automatically be caught and passed to your Express error handling middleware.

How It Works Behind the Scenes

express-async-errors uses a technique called "monkey patching" to modify Express's Router prototype. It wraps the original route methods (get, post, etc.) with versions that catch promise rejections and pass them to the next middleware.

A simplified version of what it does:

// Simplified version of how express-async-errors works
const layer = require('express/lib/router/layer');
const originalHandle = layer.prototype.handle_request;

layer.prototype.handle_request = function(req, res, next) {
  if (!this.handle.length) {
    return next();
  }
  
  try {
    const result = this.handle(req, res, next);
    if (result && typeof result.catch === 'function') {
      result.catch(err => next(err));
    }
    return result;
  } catch (error) {
    next(error);
  }
};

Real-World Example: User Management API

// app.js
const express = require('express');
require('express-async-errors');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// Routes
app.get('/users', async (req, res) => {
  const users = await User.find({});
  res.json(users);
});

app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new Error('User not found');
  }
  res.json(user);
});

app.post('/users', async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json(user);
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ message: err.message });
});

app.listen(3000);

In this example, all routes will automatically have error handling without any extra try/catch blocks.

Pros

  • Minimal setup: Just one import at the top of your app
  • No code changes required: Works with existing async route handlers
  • Cleaner route code: No need for try/catch blocks
  • Consistent error handling: All async errors are handled the same way

Cons

  • Magic factor: The error handling happens implicitly, which might confuse new developers
  • Monkey patching: Modifies Express internals, which could potentially break in future Express versions
  • All-or-nothing approach: Applies to all routes, even if some have custom error handling
  • Order dependence: Must be required after express but before creating any routes

express-async-handler: The Explicit Approach

Installation

npm install express-async-handler

Usage Example

Import the handler and wrap each async route function:

const express = require('express');
const asyncHandler = require('express-async-handler');
const router = express.Router();

// Wrap the async function with asyncHandler
router.get('/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  
  if (!user) {
    throw new Error('User not found');
  }
  
  res.json(user);
}));

router.post('/', asyncHandler(async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json(user);
}));

How It Works Behind the Scenes

express-async-handler is a simple higher-order function that wraps your async handler in a try/catch block:

// Simplified version of how express-async-handler works
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

This approach doesn't modify Express itself. Instead, it creates a new function that calls your handler and catches any errors.

Real-World Example: Product Management API

// productController.js
const Product = require('../models/Product');
const asyncHandler = require('express-async-handler');

// Notice how each function is wrapped individually
const getProducts = asyncHandler(async (req, res) => {
  const products = await Product.find({});
  res.json(products);
});

const getProductById = asyncHandler(async (req, res) => {
  const product = await Product.findById(req.params.id);
  
  if (!product) {
    res.status(404);
    throw new Error('Product not found');
  }
  
  res.json(product);
});

const createProduct = asyncHandler(async (req, res) => {
  const product = await Product.create(req.body);
  res.status(201).json(product);
});

module.exports = {
  getProducts,
  getProductById,
  createProduct
};

// productRoutes.js
const express = require('express');
const router = express.Router();
const {
  getProducts,
  getProductById,
  createProduct
} = require('../controllers/productController');

router.route('/').get(getProducts).post(createProduct);
router.route('/:id').get(getProductById);

module.exports = router;

This MVC-style approach clearly shows which functions have error handling applied.

Using with Express Middleware Pattern

express-async-handler works well with middleware functions too:

// auth.js - Authentication middleware
const jwt = require('jsonwebtoken');
const asyncHandler = require('express-async-handler');
const User = require('../models/User');

const protect = asyncHandler(async (req, res, next) => {
  let token;
  
  if (req.headers.authorization?.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1];
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Attach user to request object
    req.user = await User.findById(decoded.id).select('-password');
    
    next();
  } else {
    res.status(401);
    throw new Error('Not authorized, no token');
  }
});

Pros

  • Explicit error handling: Clear which functions have error handling
  • No monkey patching: Doesn't modify Express internals
  • Selective application: Apply to specific routes only when needed
  • Composition friendly: Works well with functional programming patterns
  • Independent of Express: Can be used with other frameworks or pure Node.js

Cons

  • More boilerplate: Requires wrapping each handler individually
  • Requires code changes: Existing code must be modified
  • Forgetting risk: Developers might forget to wrap some handlers
  • Slightly more verbose: Makes route definitions longer

Practical Implementation: Todo API Example

Let's implement the same Todo API using both libraries to see the differences.

Implementation with express-async-errors

// app.js
const express = require('express');
require('express-async-errors'); // Import early
const mongoose = require('mongoose');

// DB connection
mongoose.connect('mongodb://localhost/todo-app');

const app = express();
app.use(express.json());

// Todo Model
const Todo = mongoose.model('Todo', {
  text: String,
  completed: Boolean
});

// Routes
app.get('/todos', async (req, res) => {
  const todos = await Todo.find({});
  res.json(todos);
});

app.get('/todos/:id', async (req, res) => {
  const todo = await Todo.findById(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
});

app.post('/todos', async (req, res) => {
  const todo = await Todo.create(req.body);
  res.status(201).json(todo);
});

app.put('/todos/:id', async (req, res) => {
  const todo = await Todo.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
});

app.delete('/todos/:id', async (req, res) => {
  const todo = await Todo.findByIdAndDelete(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json({ message: 'Todo removed' });
});

// Error handler
app.use((err, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Implementation with express-async-handler

// app.js
const express = require('express');
const asyncHandler = require('express-async-handler');
const mongoose = require('mongoose');

// DB connection
mongoose.connect('mongodb://localhost/todo-app');

const app = express();
app.use(express.json());

// Todo Model
const Todo = mongoose.model('Todo', {
  text: String,
  completed: Boolean
});

// Routes
app.get('/todos', asyncHandler(async (req, res) => {
  const todos = await Todo.find({});
  res.json(todos);
}));

app.get('/todos/:id', asyncHandler(async (req, res) => {
  const todo = await Todo.findById(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
  const todo = await Todo.create(req.body);
  res.status(201).json(todo);
}));

app.put('/todos/:id', asyncHandler(async (req, res) => {
  const todo = await Todo.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
}));

app.delete('/todos/:id', asyncHandler(async (req, res) => {
  const todo = await Todo.findByIdAndDelete(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json({ message: 'Todo removed' });
}));

// Error handler
app.use((err, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

MVC-Style Implementation with express-async-handler

For larger applications, you might prefer an MVC structure. Here's how that would look with express-async-handler:

// controllers/todoController.js
const Todo = require('../models/Todo');
const asyncHandler = require('express-async-handler');

// Controller methods
const getTodos = asyncHandler(async (req, res) => {
  const todos = await Todo.find({});
  res.json(todos);
});

const getTodoById = asyncHandler(async (req, res) => {
  const todo = await Todo.findById(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
});

const createTodo = asyncHandler(async (req, res) => {
  const todo = await Todo.create(req.body);
  res.status(201).json(todo);
});

const updateTodo = asyncHandler(async (req, res) => {
  const todo = await Todo.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json(todo);
});

const deleteTodo = asyncHandler(async (req, res) => {
  const todo = await Todo.findByIdAndDelete(req.params.id);
  if (!todo) {
    res.status(404);
    throw new Error('Todo not found');
  }
  res.json({ message: 'Todo removed' });
});

module.exports = {
  getTodos,
  getTodoById,
  createTodo,
  updateTodo,
  deleteTodo
};

// routes/todoRoutes.js
const express = require('express');
const router = express.Router();
const {
  getTodos,
  getTodoById,
  createTodo,
  updateTodo,
  deleteTodo
} = require('../controllers/todoController');

router.route('/').get(getTodos).post(createTodo);
router.route('/:id').get(getTodoById).put(updateTodo).delete(deleteTodo);

module.exports = router;

// app.js
const express = require('express');
const mongoose = require('mongoose');
const todoRoutes = require('./routes/todoRoutes');

mongoose.connect('mongodb://localhost/todo-app');

const app = express();
app.use(express.json());

// Mount routes
app.use('/todos', todoRoutes);

// Error handler
app.use((err, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

This structure separates concerns nicely and makes your application more maintainable as it grows.

Best Practices and Recommendations

When to Use express-async-errors

  • Existing codebase migration: When you have many existing async route handlers and don't want to modify them all
  • Smaller applications: For simpler apps where the "magic" factor isn't a big concern
  • Prototype development: For rapid development when you want to minimize boilerplate
  • Consistent error handling: When you want all async errors to be handled in exactly the same way

When to Use express-async-handler

  • New projects: When starting fresh and can establish good patterns from the beginning
  • Larger applications: For bigger apps where explicitness and clarity are more important
  • Team environments: When working with a team where explicit code is easier to understand
  • Mixed error handling: When some routes need custom error handling while others use the default

Expert Opinions

The Express community is somewhat divided on which approach is better:

  • Advocates for express-async-errors praise its simplicity and low boilerplate.
  • Advocates for express-async-handler prefer its explicitness and lack of monkey patching.

Most Express experts agree that either option is vastly better than manual try/catch blocks everywhere.

A Hybrid Approach

Some developers use a hybrid approach:

// Create your own wrapper that uses express-async-handler internally
const asyncHandler = require('express-async-handler');

// Create wrapper functions for common operations
const createCrudHandlers = (Model) => ({
  getAll: asyncHandler(async (req, res) => {
    const items = await Model.find({});
    res.json(items);
  }),
  
  getById: asyncHandler(async (req, res) => {
    const item = await Model.findById(req.params.id);
    if (!item) {
      res.status(404);
      throw new Error(`${Model.modelName} not found`);
    }
    res.json(item);
  }),
  
  create: asyncHandler(async (req, res) => {
    const item = await Model.create(req.body);
    res.status(201).json(item);
  }),
  
  update: asyncHandler(async (req, res) => {
    const item = await Model.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!item) {
      res.status(404);
      throw new Error(`${Model.modelName} not found`);
    }
    res.json(item);
  }),
  
  delete: asyncHandler(async (req, res) => {
    const item = await Model.findByIdAndDelete(req.params.id);
    if (!item) {
      res.status(404);
      throw new Error(`${Model.modelName} not found`);
    }
    res.json({ message: `${Model.modelName} removed` });
  })
});

// Usage
const Todo = require('../models/Todo');
const todoHandlers = createCrudHandlers(Todo);

// In your routes file
router.route('/').get(todoHandlers.getAll).post(todoHandlers.create);
router.route('/:id').get(todoHandlers.getById).put(todoHandlers.update).delete(todoHandlers.delete);

This approach gives you the explicitness of express-async-handler with less repetition.

Performance and Production Considerations

Performance Impact

Both libraries have minimal performance overhead. The wrapper functions add a small amount of function call overhead, but this is negligible compared to the actual database operations or business logic in most routes.

If you're concerned about performance in extremely high-throughput APIs, consider benchmarking both approaches in your specific application.

Error Logging in Production

Regardless of which library you choose, make sure to implement proper error logging in production:

// errorMiddleware.js
const logger = require('./logger'); // Your logging solution

const errorHandler = (err, req, res, next) => {
  // Log error details
  logger.error({
    message: err.message,
    stack: err.stack,
    method: req.method,
    path: req.path,
    ip: req.ip,
    user: req.user?.id // If using authentication
  });
  
  // Send appropriate response to client
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({
    message: err.message,
    // Only show stack trace in development
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

Custom Error Classes

Both libraries work well with custom error classes:

// errors/index.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404);
  }
}

class BadRequestError extends AppError {
  constructor(message = 'Bad request') {
    super(message, 400);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401);
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403);
  }
}

module.exports = {
  AppError,
  NotFoundError,
  BadRequestError,
  UnauthorizedError,
  ForbiddenError
};

// Usage with express-async-handler
const { NotFoundError } = require('../errors');
const asyncHandler = require('express-async-handler');

const getTodoById = asyncHandler(async (req, res) => {
  const todo = await Todo.findById(req.params.id);
  if (!todo) {
    throw new NotFoundError('Todo not found');
  }
  res.json(todo);
});

This approach gives you more control over error types and status codes.

Advanced Topics and Integration

TypeScript Integration

Both libraries work well with TypeScript, but express-async-handler provides better type safety:

// With express-async-handler
import express, { Request, Response } from 'express';
import asyncHandler from 'express-async-handler';
import { User } from '../models/User';

// Type-safe handler
const getUser = asyncHandler(async (req: Request, res: Response) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }
  res.json(user);
});

// With express-async-errors (still type-safe but less explicit)
import express, { Request, Response } from 'express';
import 'express-async-errors';
import { User } from '../models/User';

// Type information comes from Express types
const getUser = async (req: Request, res: Response) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }
  res.json(user);
};

Testing Considerations

When testing route handlers, the error handling approach can affect your test setup:

Testing with express-async-errors

// tests/userRoutes.test.js
const request = require('supertest');
const express = require('express');
require('express-async-errors'); // Must be included in test setup
const mongoose = require('mongoose');
const User = require('../models/User');

// Create test app
const app = express();
app.use(express.json());

// Add routes
app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }
  res.json(user);
});

// Add error handler
app.use((err, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({ message: err.message });
});

// Test
describe('User routes', () => {
  it('returns 404 for non-existent user', async () => {
    const res = await request(app).get('/users/nonexistentid');
    expect(res.statusCode).toBe(404);
    expect(res.body.message).toBe('User not found');
  });
});

Testing with express-async-handler

// tests/userRoutes.test.js
const request = require('supertest');
const express = require('express');
const asyncHandler = require('express-async-handler');
const mongoose = require('mongoose');
const User = require('../models/User');

// Create test app
const app = express();
app.use(express.json());

// Add routes
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }
  res.json(user);
}));

// Add error handler
app.use((err, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode).json({ message: err.message });
});

// Test
describe('User routes', () => {
  it('returns 404 for non-existent user', async () => {
    const res = await request(app).get('/users/nonexistentid');
    expect(res.statusCode).toBe(404);
    expect(res.body.message).toBe('User not found');
  });
});

Both approaches work similarly in tests, but with express-async-errors, you need to ensure it's imported before your routes are defined.

Middleware Chaining

Both libraries work well with middleware chains, but express-async-handler gives more explicit control:

// With express-async-handler
const protect = asyncHandler(async (req, res, next) => {
  // Authentication logic
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    res.status(401);
    throw new Error('Not authorized');
  }
  
  // Verify token and attach user to request
  req.user = await verifyToken(token);
  next();
});

app.get('/profile', protect, asyncHandler(async (req, res) => {
  // req.user is guaranteed to exist here
  const user = await User.findById(req.user.id).select('-password');
  res.json(user);
}));

Creating Your Own Async Handler

Understanding how these libraries work can help you create your own custom solution if needed:

Simple Custom Async Handler

// utils/asyncHandler.js
/**
 * Wraps an async function to automatically catch errors and pass to Express next()
 * @param {Function} fn - Async express route handler/middleware
 * @returns {Function} - Wrapped function
 */
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

This is essentially what express-async-handler does. You can extend it with your own functionality if needed.

Extended Custom Handler with Logging

// utils/asyncHandler.js
const logger = require('./logger');

/**
 * Wraps an async function with error handling and logging
 * @param {Function} fn - Async express route handler/middleware
 * @returns {Function} - Wrapped function
 */
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch((error) => {
    // Log error details
    logger.error({
      error: error.message,
      stack: error.stack,
      method: req.method,
      path: req.path,
      body: req.body,
      params: req.params,
      query: req.query,
      ip: req.ip
    });
    
    next(error);
  });
};

module.exports = asyncHandler;

This extended version adds logging before passing the error to the next middleware.

Creating Your Own Global Handler (Like express-async-errors)

// utils/setupAsyncErrorHandling.js
/**
 * Patches Express router to handle async errors globally
 * @param {object} express - Express module
 */
function setupAsyncErrorHandling(express) {
  const Layer = require('express/lib/router/layer');
  const originalHandle = Layer.prototype.handle_request;
  
  Layer.prototype.handle_request = function(req, res, next) {
    if (!this.handle || !this.handle.length) {
      return next();
    }
    
    try {
      const result = this.handle(req, res, next);
      if (result && typeof result.catch === 'function') {
        result.catch((err) => next(err));
      }
      return result;
    } catch (err) {
      next(err);
    }
  };
}

module.exports = setupAsyncErrorHandling;

// Usage
const express = require('express');
require('./utils/setupAsyncErrorHandling')(express);
const app = express();

This approach gives you the benefits of express-async-errors but with more control over the implementation.

Conclusion and Recommendations

Summary of Differences

Aspect express-async-errors express-async-handler
Setup Global (one import) Per-handler
Code Changes Minimal More extensive
Explicitness Less explicit More explicit
Implementation Monkey patching Higher-order function
Selective Application No (all or nothing) Yes (per handler)
TypeScript Integration Good Better

Final Recommendation

Both libraries are excellent solutions to the async error handling problem in Express. Your choice should depend on your specific needs:

  • Choose express-async-errors if:
    • You have a large existing codebase with many async handlers
    • You want minimal code changes
    • You prefer a set-it-and-forget-it approach
    • Your team is comfortable with "magical" solutions that work behind the scenes
  • Choose express-async-handler if:
    • You're starting a new project
    • You value explicitness and clarity over brevity
    • You need selective application of error handling
    • You work in a team environment where code readability is important
    • You're using TypeScript and value strong typing

In very large projects, consider the hybrid approach: create your own wrapper functions that use express-async-handler internally to get the best of both worlds.

The Car Analogy

Think of express-async-errors as an automatic transmission in a car: it handles all the gear shifting for you behind the scenes. You just need to focus on driving (your business logic).

express-async-handler is more like a manual transmission: you have more explicit control, and it's clear to anyone looking at your code exactly what's happening.

Both will get you to your destination, but the driving experience is different. Some developers prefer the control of a manual transmission, while others prefer the convenience of an automatic.

Further Reading