{"id":"text21/safecall","name":"safecall","scope":"text21","platform":"roblox","description":"A library for creating error and memory safe code.","version":"1.2.0","latest":"1.2.0","versions":["1.2.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"a6d1ba96411dc747c1d5798a3ec433412f35ebfd3df16ba2bac2a4284afc9361","likes":0,"downloads":0,"install":"forest install text21/safecall","url":"https://forest.dev/p/roblox/text21/safecall","files":"https://api.forest.dev/ai/package/roblox/text21/safecall/files","readme":"![image|500x500, 100%](upload://hSePoNhAhKYQI5QSoKvPupRKso0.png)\n\n# Safecall\n\n## Overview\n\n**SafeCall** is a lightweight, great error handling wrapper for Roblox Lua. It simplifies safe function execution by automatically handling errors, retries, async support, rate limiting, profiling, and more, reducing crashes and improving debugging.\n\nIt works standalone or integrates seamlessly with other frameworks (e.g. Promise, ProfileStore, Knit)\n\n## Installation\n\n- Download the rbxm\n- Ungroup the **ReplicatedStorage model** and place the `Main` folder inside `ReplicatedStorage`.\n- Also, place `SafeCallExample` content inside `ServerScriptService`.\n\nRequire it:\n\n```\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal SafeCall = require(ReplicatedStorage.Main.Modules.SafeCall)\nlocal safe = SafeCall.new()\n```\n\n## API Reference\n\n### Constructor\n\n- `SafeCall.new(logFunction: function?) -> SafeCall`\n\nCreates a new SafeCall instance.\n\n- `logFunction` (optional): custom error logger, defaults to `warn`.\n\n### Instance Methods\n\n<details>\n<summary><strong>Call Method</strong></summary>\n\n**Call**\n\n```\nsuccess, result = safe:Call(fn: function, ...any) -> (bool, any)\n```\n\nCalls `fn` safely with arguments. Returns success status and result or error.\n\nNote: `Call` and related helpers preserve the full tuple of return values from `fn` (including `nil` values). If `fn` returns multiple values, `safe:Call(fn)` will return the same tuple.\n\n**CallWithRetry**\n\n```\nsuccess, result = safe:CallWithRetry(fn: function, attempts?: number, delay?: number, backoff?: number, ...any)\n```\n\nCalls `fn` with retry logic on failure.\n\n- `attempts`: number of retries (default 3)\n- `delay`: initial wait between retries (default 0.1s)\n- `backoff`: multiplier to increase delay after each retry (default 1.5)\n\nExtras:\n\n- The retry handler (set via `safe:SetRetryHandler`) receives `(err, attempt, attempts, delay)` and may return `false` to abort further retries or a numeric value to override the next delay. This lets you implement dynamic backoff or conditional aborts.\n- `CallWithRetry` preserves full multi-value returns when the wrapped function succeeds.\n\n**CallAsync**\n\n```\npromise = safe:CallAsync(fn: function, ...any) -> Promise\n```\n\nCalls `fn` async, returns a Promise that resolves or rejects based on call success.\nRequires the `Promise` library.\n\nNote: `CallAsync` resolves with the full tuple returned by `fn` (preserving `nil` values). The project marks `CallAsync` and `SetPromiseModule` as deprecated in favor of `CallWithThread` for simple deferred execution — keep using a Promise adapter only if you need Promise-based flows.\n\n**Use case**: Run a function asynchronously and handle its result with Promises.\n**Best for**: Async workflows like data fetching or web requests.\n\n## Example\n\n```\nsafe:CallAsync(function()\n\treturn someAsyncFunction()\nend):andThen(function(result)\n\tprint(\"Got result:\", result)\nend):catch(warn)\n```\n\nRequires the Promise library. Errors are caught and passed to `.catch`.\n\n**CallDeferred**\n\n```\nsafe:CallDeferred(fn: function, ...any)\nSchedules `fn` to be called safely in a deferred (non-blocking) manner.\n```\n\n**Use case**: Runs your function safely after the current thread yields.\n**Best for**: Non-blocking operations like safe event dispatching.\n\n## Example\n\n```\nsafe:CallDeferred(function()\n\tprint(\"Runs later, but safely!\")\nend)\n```\n\nIt’s like `task.defer` but with error safety built-in.\n\n**CallDelayed**\n\n```\nsuccess, result = safe:CallDelayed(delay: number, fn: function, ...any)\n```\n\nWaits `delay` seconds then calls `fn` safely.\n\n## Example\n\n```\nsafe:CallDelayed(2, function()\n\tprint(\"Called after 2 seconds.\")\nend)\n```\n\n**Use case**: Automatically wraps **all functions inside a table** with SafeCall error handling.\n**Best for**: Making whole utility modules or service interfaces crash-safe without rewriting every function.\n\n**ProtectTable**\n\n```\nprotectedTable = safe:ProtectTable(tbl: table)\n```\n\nReturns a version of `tbl` where all functions are wrapped with safe calls.\n**Behavior changes**: `ProtectTable` now returns a proxy that preserves colon-call (`:`) semantics and respects metamethods on the original table. Methods will be invoked with the original table as `self`, and non-function fields remain accessible and writable on the original table.\n**Use case**: Automatically wraps **all functions inside a table** with SafeCall error handling while preserving method bindings.\n**Best for**: Making whole utility modules or service interfaces crash-safe without rewriting every function.\n\n## Example\n\n```\nlocal unsafeUtils = {\n\tPrintHello = function()\n\t\tprint(\"Hello\")\n\tend,\n\tBreakIt = function()\n\t\terror(\"This will crash!\")\n\tend\n}\n\nlocal safeUtils = safe:ProtectTable(unsafeUtils)\n\nsafeUtils.PrintHello() --> works normally\nsafeUtils.BreakIt()    --> error is caught, doesn't crash\n```\n\n**WrapEvent**\n\n```\nconnection = safe:WrapEvent(remote: RemoteEvent | BindableEvent, callback: function)\n```\n\nWraps a remote or bindable event callback with safe error handling.\n**Use case**: Wraps RemoteEvent or BindableEvent connections with SafeCall.\n**Best for**: Secure remote handling to prevent crashes from bad data.\n\n## Example\n\n```\nsafe:WrapEvent(RemoteEvent, function(player, data)\n\tprint(player.Name, data)\nend)\n\n```\n\n**WrapFunction**\n\n```\nsafe:WrapFunction(remote: RemoteFunction | BindableFunction, callback: function)\n```\n\nWraps a remote or bindable function invocation callback safely.\n**Use case**: Wraps RemoteFunction or BindableFunction callbacks.\n**Best for**: Validating or securing remote/bindable invokes.\n\n## Example\n\n```\nsafe:WrapFunction(RemoteFunction, function(player, request)\n\treturn processRequest(request)\nend)\n```\n\n**CallBatch**\n\n```\nresults = safe:CallBatch(functions: {function})\n```\n\nCalls a list of functions safely, returning a table of success/result pairs.\n**Use case**: Executes a batch of functions safely, returns all results.\n**Best for**: Running multiple tasks (e.g. setup, cleanup) with error isolation.\n\nDetails: Each entry in `results` is a packed-result table (use `table.unpack(results[i], 1, results[i].n)` to retrieve the full return tuple for that function). This preserves multiple return values per function in the batch.\n\n## Example\n\n```\nlocal results = safe:CallBatch({\n\tfunction() return \"ok1\" end,\n\tfunction() error(\"bad2\") end,\n\tfunction() return \"ok3\" end,\n})\n```\n\n**CallWithTimeout**\n\n```\nsuccess, result = safe:CallWithTimeout(timeout: number, fn: function, ...any)\n```\n\nCalls `fn` safely but aborts if it exceeds `timeout` seconds.\n**Use case**: Ensures a function doesn’t run forever — fails if it takes too long.\n**Best for**: External service calls, long waits.\n\nNote: `CallWithTimeout` preserves the full return tuple from the function if it completes before the timeout.\n\n## Example\n\n```\nlocal success, result = safe:CallWithTimeout(5, function()\n\twhile true do task.wait() end\nend)\n```\n\n**Circuit Breaker**\n\n```\nbreaker = safe:CreateCircuitBreaker(threshold?: number, resetTime?: number)\n\nsuccess, result = safe:CallWithCircuitBreaker(breaker, fn: function, ...any)\n```\n\nCreates a circuit breaker to stop calling `fn` if repeated failures occur, then resets after cooldown.\n**Use case**: Temporarily disables a failing function after repeated errors.\n**Best for**: External APIs, datastores, unstable services.\n\n## Example\n\n```\nlocal breaker = safe:CreateCircuitBreaker(3, 10) -- 3 fails, 10s cooldown\n\nsafe:CallWithCircuitBreaker(breaker, function()\n\treturn ExternalAPI()\nend)\n```\n\n**Rate Limiter**\n\n```\nlimiter = safe:CreateRateLimiter(maxCalls?: number, timeWindow?: number)\n\nsuccess, result = safe:CallWithRateLimit(limiter, fn: function, ...any)\n```\n\nSafely connects to Roblox events/signals with automatic error handling and optional weak reference disconnect.\n**Use case**: Limits how often a function can run within a time window.\n**Best for**: Anti-spam, cooldowns, external APIs.\n\n## Example\n\n```\nlocal limiter = safe:CreateRateLimiter(5, 10) -- Max 5 calls per 10s\n\nsafe:CallWithRateLimit(limiter, function()\n\tprint(\"Allowed call\")\nend)\n```\n\n**ConnectSafe**\n\n```\nconnection = safe:ConnectSafe(\n\tsignal: RBXScriptSignal,\n\tcallback: (...any) -> (),\n\toptions: {\n\t\tweakRef: Instance?,\n\t\tusePromise: boolean?\n\t}?\n)\n```\n\nSafely connects to Roblox events/signals with automatic error handling and optional weak reference disconnect.\n**Use case**: Safely connects to events/signals.\n**Best for**: Cleaner `.Changed`, `.Touched`, or custom signal connections.\n\n## Example\n\n```\nBaisc\nsafe:ConnectSafe(part.Touched, function(hit)\n\tprint(\"Touched:\", hit)\n\terror(\"Test error\") -- will be caught and logged\nend)\n\nWith weakRef\nsafe:ConnectSafe(button.MouseButton1Click, function()\n\tprint(\"Button clicked\")\nend, {\n\tweakRef = button,\n})\n\n\nWith Promise mode\nsafe:SetPromiseModule(Promise)\n\nsafe:ConnectSafe(remote.OnClientEvent, function(data)\n\tprint(\"Got data:\", data)\n\terror(\"Promise test\")\nend, {\n\tusePromise = true,\n})\n\n```\n\n**Memoize**\n\n```\nmemoizedFn = safe:Memoize(fn: function, ttl?: number)\n```\n\nReturns a memoized version of `fn` with cache TTL (time-to-live).\n**Use case**: Caches results from a function to avoid repeating work.\n**Best for**: Expensive calculations, function caching.\n\n## Example\n\n```\nlocal slowFn = safe:Memoize(function(x)\n\ttask.wait(2)\n\treturn x * 2\nend, 10)\n```\n\n**Profiling**\n\n```\nprofiler = safe:CreateProfiler()\n\nsuccess, result = safe:CallWithProfiler(profiler, fn: function, ...any)\n\nstats = safe:GetProfilerStats(profiler)\n```\n\nProfile calls for performance and error stats.\n**Use case**: Measure performance and error stats of your functions.\n**Best for**: Debugging slow or unstable code.\n\n```\nlocal profiler = safe:CreateProfiler()\n\nsafe:CallWithProfiler(profiler, function()\n\ttask.wait(0.5)\n\terror(\"whoops\")\nend)\n\nprint(safe:GetProfilerStats(profiler))\n```\n\n**Global Error Handlers**\n\n```\nsafe:AddGlobalHandler(handler: function)\nsafe:RemoveGlobalHandler(handler: function)\n```\n\nAdd or remove global error handlers that are called on every error.\nA **global error handler** is a function that runs **every time any `safe:Call()` fails**, no matter where it's called in your game.\n\nThink of it like a **global listener** for all uncaught errors in SafeCall.\n\n## Example\n\n```\nlocal function globalLogger(err, traceback)\n\tprint(\"GLOBAL ERROR:\", err)\n\tprint(\"Traceback:\\n\", traceback)\nend\n\nsafe:AddGlobalHandler(globalLogger)\n\nsafe:Call(function()\n\terror(\"Something broke!\")\nend)\n```\n\n</details>\n\n<details>\n<summary><strong>Usage Examples</strong></summary>\n\n## Usage Examples\n\n# Simple safe call\n\n```\nsafe:Call(function()\n\terror(\"Oops!\")\nend)\n-- Output: Warning printed, no crash\n```\n\n# Safe remote event handling\n\n```\nlocal remote = game.ReplicatedStorage:WaitForChild(\"SomeEvent\")\nsafe:WrapEvent(remote, function(player, data)\n\tprint(player.Name, data)\nend)\n```\n\n# Safe async Promise call\n\n```\nsafe:CallAsync(function()\n\treturn Promise.new(function(resolve, reject)\n\t\t-- async logic here\n\t\tresolve(\"Done\")\n\tend)\nend):andThen(print):catch(warn)\n```\n\n# Use with retry\n\n```\nsafe:CallWithRetry(function()\n\t-- unstable operation\nend, 5, 0.2, 2)\n```\n\n# webhook\n\n```\nlocal HttpService = game:GetService(\"HttpService\")\nlocal webhookUrl = \"YOUR_DISCORD_WEBHOOK_URL\"\n\nlocal function webhookLogger(err)\n    local payload = HttpService:JSONEncode({\n        username = \"SafeCall Logger\",\n        embeds = {{\n            title = \"SafeCall Error\",\n            description = tostring(err),\n            color = 16711680, -- red\n            timestamp = os.date(\"!%Y-%m-%dT%H:%M:%SZ\"),\n        }}\n    })\n\n    pcall(function()\n        HttpService:PostAsync(webhookUrl, payload, Enum.HttpContentType.ApplicationJson)\n    end)\nend\n\nlocal safe = SafeCall.new(webhookLogger)\n```\n","readmeTruncated":false}