or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-utilities.mdcore-abilities.mderror-handling.mdindex.mdpermission-checking.mdrule-building.md
tile.json

error-handling.mddocs/

Error Handling

Handle permission violations with detailed error context, customizable messages, and utility methods for authorization failures.

Capabilities

ForbiddenError Class

Error class for permission violations with detailed context and customizable error messages.

/**
 * Error class for permission violations
 */
class ForbiddenError<T extends AnyAbility = AnyAbility> extends Error {
  /** The ability instance that determined the permission was forbidden */
  readonly ability: T;
  
  /** The action that was attempted */
  readonly action: string;
  
  /** The subject the action was attempted on */
  readonly subject: any;
  
  /** The specific field if field-level access was attempted */
  readonly field?: string;
  
  /** The detected type of the subject */
  readonly subjectType: string;

  /**
   * Create a ForbiddenError instance
   * @param ability - The ability instance
   */
  constructor(ability: T);
}

Usage Examples:

import { createMongoAbility, ForbiddenError } from "@casl/ability";

const ability = createMongoAbility([
  { action: 'read', subject: 'Post' },
  { action: 'update', subject: 'Post', conditions: { authorId: 'user123' } },
  { action: 'delete', subject: 'Post', inverted: true, conditions: { published: true } }
]);

// Manual error creation using from() method
const post = { __type: 'Post', id: 1, authorId: 'other', published: true };

try {
  if (ability.cannot('update', post)) {
    // Check stores context, then create error
    const error = ForbiddenError.from(ability);
    throw error;
  }
} catch (error) {
  if (error instanceof ForbiddenError) {
    console.log(error.action); // Last checked action
    console.log(error.subject); // Last checked subject
    console.log(error.subjectType); // Detected subject type
    console.log(error.ability === ability); // true
    console.log(error.message); // Default message
  }
}

// Direct constructor usage (requires setting properties manually)
try {
  const error = new ForbiddenError(ability);
  // Properties would need to be set based on last permission check
  throw error;
} catch (error) {
  if (error instanceof ForbiddenError) {
    console.log(error.ability === ability); // true
  }
}

Static Factory Methods

Static methods for creating ForbiddenError instances and configuring default messages.

/**
 * Static methods for ForbiddenError creation and configuration
 */
interface ForbiddenErrorStatic {
  /**
   * Set the default error message template or generator function
   * @param messageOrFn - Static message string or function to generate messages
   */
  setDefaultMessage(messageOrFn: string | ErrorMessageFactory): void;
  
  /**
   * Create a ForbiddenError from an ability instance using the last failed check
   * @param ability - Ability instance to create error from
   * @returns New ForbiddenError instance
   */
  from<U extends AnyAbility>(ability: U): ForbiddenError<U>;
}

/**
 * Function type for generating custom error messages
 */
type ErrorMessageFactory = (error: {
  action: string;
  subject: any;
  subjectType: string;
  field?: string;
}) => string;

Usage Examples:

import { createMongoAbility, ForbiddenError } from "@casl/ability";

// Set custom default message
ForbiddenError.setDefaultMessage(
  ({ action, subjectType, field }) => {
    const fieldPart = field ? ` field "${field}" of` : '';
    return `Access denied: cannot ${action}${fieldPart} ${subjectType}`;
  }
);

// Set simple string message
ForbiddenError.setDefaultMessage('Insufficient permissions');

// Create error from ability (requires recent failed check)
const ability = createMongoAbility([
  { action: 'read', subject: 'Post' }
]);

const post = { __type: 'Post', id: 1 };

// This will store the failed check internally
const canDelete = ability.can('delete', post); // false

if (!canDelete) {
  const error = ForbiddenError.from(ability);
  console.log(error.action); // 'delete'
  console.log(error.subject); // post object
  console.log(error.message); // Custom message
}

// Custom message factory with context
ForbiddenError.setDefaultMessage(({ action, subjectType, field, subject }) => {
  const context = [];
  
  if (field) {
    context.push(`field "${field}"`);
  }
  
  if (subject?.id) {
    context.push(`${subjectType} ${subject.id}`);
  } else {
    context.push(subjectType);
  }
  
  const contextStr = context.join(' of ');
  return `Permission denied: cannot ${action} ${contextStr}`;
});

const restrictedPost = { __type: 'Post', id: 42, title: 'Secret' };
ability.can('delete', restrictedPost); // false, stores context

const contextualError = ForbiddenError.from(ability);
console.log(contextualError.message); // "Permission denied: cannot delete Post 42"

Instance Methods

Methods for customizing error messages and handling permission checks with automatic error throwing.

/**
 * Instance methods for ForbiddenError
 */
interface ForbiddenErrorInstance<T extends AnyAbility> {
  /**
   * Set a custom error message for this specific error instance
   * @param message - Custom error message
   * @returns This error instance for chaining
   */
  setMessage(message: string): this;
  
  /**
   * Throw this error unless the specified action is allowed
   * @param action - Action to check
   * @param subject - Subject to check (optional)
   * @param field - Field to check (optional)
   * @throws ForbiddenError if action is not allowed
   */
  throwUnlessCan(...args: Parameters<T['can']>): void;
  
  /**
   * Return this error unless the specified action is allowed
   * @param action - Action to check
   * @param subject - Subject to check (optional) 
   * @param field - Field to check (optional)
   * @returns This error if action is not allowed, undefined if allowed
   */
  unlessCan(...args: Parameters<T['can']>): this | undefined;
}

Usage Examples:

import { createMongoAbility, ForbiddenError } from "@casl/ability";

const ability = createMongoAbility([
  { action: 'read', subject: 'Post' },
  { action: 'update', subject: 'Post', conditions: { authorId: 'user123' } }
]);

const post = { __type: 'Post', id: 1, authorId: 'other' };

// Custom message for specific error
const error = new ForbiddenError(ability, 'update', post)
  .setMessage('You can only update your own posts');

console.log(error.message); // 'You can only update your own posts'

// Throw unless permission granted
try {
  new ForbiddenError(ability, 'update', post)
    .setMessage('Update not allowed for this post')
    .throwUnlessCan('update', post);
} catch (thrownError) {
  console.log(thrownError.message); // 'Update not allowed for this post'
}

// Conditional error return
const conditionalError = new ForbiddenError(ability, 'delete', post)
  .setMessage('Delete operation not permitted')
  .unlessCan('delete', post);

if (conditionalError) {
  console.log('Delete is forbidden:', conditionalError.message);
} else {
  console.log('Delete is allowed');
}

// Permission checking with automatic error throwing
function updatePost(postData: any, userId: string) {
  const ability = createMongoAbility([
    { action: 'update', subject: 'Post', conditions: { authorId: userId } }
  ]);
  
  // This will throw if user cannot update the post
  ForbiddenError.from(ability)
    .setMessage(`User ${userId} cannot update this post`)
    .throwUnlessCan('update', postData);
  
  // Update logic here - only reached if permission granted
  console.log('Updating post:', postData.id);
}

// Usage
try {
  updatePost({ __type: 'Post', id: 1, authorId: 'other' }, 'user123');
} catch (error) {
  console.log(error.message); // Permission error
}

updatePost({ __type: 'Post', id: 2, authorId: 'user123' }, 'user123'); // Success

Integration Patterns

Common patterns for integrating error handling into application logic.

/**
 * Utility patterns for error handling integration
 */
interface ErrorHandlingPatterns {
  /** Middleware pattern for express-like frameworks */
  middleware: (req: any, res: any, next: any) => void;
  
  /** Guard pattern for method protection */
  guard: <T>(action: string, subject: any, operation: () => T) => T;
  
  /** Decorator pattern for class methods */
  authorize: (action: string, subjectField?: string) => MethodDecorator;
}

Usage Examples:

import { createMongoAbility, ForbiddenError } from "@casl/ability";

// Express middleware pattern
function createAuthMiddleware(getAbility: (req: any) => any) {
  return (action: string, subject?: string) => {
    return (req: any, res: any, next: any) => {
      try {
        const ability = getAbility(req);
        const targetSubject = subject || req.params.resource;
        
        ForbiddenError.from(ability)
          .setMessage(`Access denied for ${action} on ${targetSubject}`)
          .throwUnlessCan(action, targetSubject);
          
        next();
      } catch (error) {
        if (error instanceof ForbiddenError) {
          res.status(403).json({ error: error.message });
        } else {
          next(error);
        }
      }
    };
  };
}

// Usage with Express
// app.get('/posts/:id', 
//   createAuthMiddleware(getUserAbility)('read', 'Post'), 
//   getPostController
// );

// Guard pattern for business logic
function createPermissionGuard(ability: any) {
  return <T>(action: string, subject: any, operation: () => T): T => {
    ForbiddenError.from(ability)
      .setMessage(`Operation "${action}" is not permitted`)
      .throwUnlessCan(action, subject);
    
    return operation();
  };
}

// Usage
const ability = createMongoAbility([
  { action: 'delete', subject: 'Post', conditions: { authorId: 'user123' } }
]);

const guard = createPermissionGuard(ability);

try {
  const result = guard('delete', { __type: 'Post', authorId: 'user123' }, () => {
    // Protected operation
    console.log('Deleting post...');
    return { success: true };
  });
  console.log(result); // { success: true }
} catch (error) {
  console.log('Access denied:', error.message);
}

// Service layer pattern
class PostService {
  constructor(private ability: any) {}
  
  async deletePost(postId: string) {
    const post = await this.findPost(postId);
    
    // Check permission before operation
    if (this.ability.cannot('delete', post)) {
      throw new ForbiddenError(this.ability, 'delete', post)
        .setMessage(`Cannot delete post ${postId}: insufficient permissions`);
    }
    
    // Perform deletion
    return this.performDelete(post);
  }
  
  async updatePost(postId: string, data: any) {
    const post = await this.findPost(postId);
    
    // Use throwUnlessCan for automatic error handling
    new ForbiddenError(this.ability, 'update', post)
      .setMessage(`Cannot update post ${postId}`)
      .throwUnlessCan('update', post);
    
    return this.performUpdate(post, data);
  }
  
  private async findPost(id: string) {
    // Mock implementation
    return { __type: 'Post', id, authorId: 'user123' };
  }
  
  private async performDelete(post: any) {
    console.log('Deleting:', post.id);
    return { deleted: true };
  }
  
  private async performUpdate(post: any, data: any) {
    console.log('Updating:', post.id, data);
    return { ...post, ...data };
  }
}

// Usage
const service = new PostService(ability);

try {
  await service.deletePost('post1');
  await service.updatePost('post2', { title: 'New Title' });
} catch (error) {
  if (error instanceof ForbiddenError) {
    console.log('Permission error:', error.message);
  }
}