log_error Decorator¶
log_error() catches any exception raised by the decorated
main function, writes a timestamped traceback to a rotating log file, then
re-raises the exception unchanged. On the happy path the decorator is fully
transparent — the return value and behaviour of the function are identical to
the unwrapped version.
Why You Need This¶
Alfred Script Filters run inside Alfred’s own process. When your Python code
raises an exception Alfred silently swallows it — the Drop Down Menu goes blank
and there is no traceback visible anywhere. log_error()
gives you a persistent, on-disk record of every error so you can open the log
file and see exactly which line failed.
Usage¶
Minimal — write to the default log file ~/.alfred-afwf/error.log:
import afwf.api as afwf
@afwf.log_error()
def main(query: str) -> afwf.ScriptFilter:
...
Custom log file — useful when a workflow has multiple Script Filters and you want to keep their error logs separate:
import afwf.api as afwf
@afwf.log_error(log_file="~/.alfred-afwf/search_bookmarks.log")
def main(query: str) -> afwf.ScriptFilter:
...
Limit traceback depth — keeps the log compact when call stacks are deep:
import afwf.api as afwf
@afwf.log_error(log_file="~/.alfred-afwf/my_workflow.log", tb_limit=5)
def main(query: str) -> afwf.ScriptFilter:
...
Control log rotation — lower max_bytes for tighter disk budgets:
import afwf.api as afwf
@afwf.log_error(
log_file="~/.alfred-afwf/search_bookmarks.log",
max_bytes=200_000,
backup_count=1,
)
def main(query: str) -> afwf.ScriptFilter:
...
Log Format¶
Each exception appends one entry to the log file:
[2026-04-08 10:23:45]
Traceback (most recent call last):
File ".../search_bookmarks.py", line 33, in main
raise ValueError("This is a simulated Python error triggered by query='error'")
ValueError: This is a simulated Python error triggered by query='error'
------------------------------------------------------------
The timestamp [YYYY-MM-DD HH:MM:SS] is followed by the full Python
traceback, and a 60-character separator line is appended so multiple entries
remain easy to tell apart.
Log Rotation¶
log_error() uses logging.handlers.RotatingFileHandler
under the hood. Once the active log file exceeds max_bytes (default
500 000 bytes ≈ 500 KB) it is rotated: the current file is renamed to .1,
the previous .1 becomes .2, and so on. Files beyond backup_count
(default 2) are deleted automatically. Total disk usage is bounded at
max_bytes × (backup_count + 1), roughly 1.5 MB with the defaults:
~/.alfred-afwf/search_bookmarks.log ← current (newest)
~/.alfred-afwf/search_bookmarks.log.1
~/.alfred-afwf/search_bookmarks.log.2 ← oldest, deleted on next rotation
The handler is thread-safe and initialised lazily — no file I/O occurs on the
happy path, so there is no measurable overhead per uvx invocation.
Full Example¶
The following is the complete afwf/examples/search_bookmarks.py. Passing
query="error" deliberately raises an exception so you can verify that the
log file is written correctly:
# -*- coding: utf-8 -*-
"""
Example: Search Bookmarks
=========================
**What it demonstrates**
Shows how to build a fuzzy-search Script Filter using
:mod:`afwf.opt.fuzzy_item`. A static list of bookmarks is turned into
:class:`afwf.opt.fuzzy_item.Item` objects; when the user types a query the
list is narrowed with :class:`afwf.opt.fuzzy_item.FuzzyItemMatcher`. If no
fuzzy match is found the full list is returned so the user always sees
results. Selecting an item opens the URL in the default browser via the
``open_url`` variable pair. Type ``error`` as the query to trigger a
simulated error and see how :func:`afwf.log_error` writes a traceback to a
log file.
"""
import afwf.api as afwf
import afwf.opt.fuzzy_item.api as fuzzy_item
BOOKMARKS = [
("Alfred App", "https://www.alfredapp.com/"),
("Python", "https://www.python.org/"),
("GitHub", "https://github.com/"),
("Stack Overflow", "https://stackoverflow.com/"),
("MDN Web Docs", "https://developer.mozilla.org/"),
("PyPI", "https://pypi.org/"),
("Read the Docs", "https://readthedocs.org/"),
("Hacker News", "https://news.ycombinator.com/"),
("Wikipedia", "https://www.wikipedia.org/"),
("Google", "https://www.google.com/"),
("YouTube", "https://www.youtube.com/"),
("Twitter / X", "https://twitter.com/"),
("Reddit", "https://www.reddit.com/"),
("AWS Console", "https://console.aws.amazon.com/"),
("Docker Hub", "https://hub.docker.com/"),
("Homebrew", "https://brew.sh/"),
("VS Code Docs", "https://code.visualstudio.com/docs"),
("Real Python", "https://realpython.com/"),
("Anthropic Claude", "https://claude.ai/"),
("OpenAI", "https://openai.com/"),
]
@afwf.log_error(
log_file=afwf.path_enum.dir_home.joinpath(
".alfred-afwf/search_bookmarks.log"
), # or just @log_error()
)
def main(query: str) -> afwf.ScriptFilter:
if query.strip() == "error":
raise ValueError("This is a simulated Python error triggered by query='error'")
items = []
for title, url in BOOKMARKS:
item = fuzzy_item.Item(title=title, subtitle=url, arg=url)
item.set_fuzzy_match_name(title)
item.open_url(url)
items.append(item)
if query.strip():
matcher = fuzzy_item.FuzzyItemMatcher.from_items(items)
matched = matcher.match(query, threshold=0)
result_items = matched if matched else items
else:
result_items = items
sf = afwf.ScriptFilter()
sf.items.extend(result_items)
return sf