Serverless-first. Built for cold starts, tight memory, and one binary.Deploy guide →

Errors and exit codes

There are no custom error classes in Fairyfly. You get QuickJS's built-ins — Error, TypeError, SyntaxError, RangeError — plus a handful of APIs that reject with plain strings.

Uncaught errors

An exception that escapes your script prints the message and stack to stderr, then the process exits 1:

// ff boom.js
throw new Error("boom");
$ ff boom.js
Error: Error: boom
    at <eval> (boom.js:1:11)

$ echo $?
1

Note the doubled Error: Error: prefix, the at <eval> frame carrying file:line:column, and the blank line before the shell prompt.

try / catch works as usual — for the APIs listed below.

What throws (and what doesn't)

APIBehaviour
setTimeout(fn), queueMicrotask(nonFn)throws TypeError — caught by try/catch
Database.open() without a paththrows Database.open requires a file path
new Request() without a URLthrows Request requires a URL string as first argument
new Worker() without a paththrows Worker requires a module path
sql template with no poolthrows no database pool
Postgres tx after commit()throws transaction is closed
fs.readFile / writeFile / mkdir / rm / readdirdoes not throw — returns undefined on failure
fs.readFileAsync() etc.rejects with the error name, e.g. FileNotFound — but a successful call hangs the process afterwards (Known issue; see fs)

Sync fs swallows errors today. A missing file, a bad directory, an unreadable path — all return undefined with no exception. Always check fs.exists() first, or use the Promise variants, which report the real error:

// Reliable: async variants reject with the error name
try {
  const text = await fs.readFileAsync("config.json", "utf8");
} catch (e) {
  console.error("cannot read config:", e.message); // "FileNotFound"
}

// Sync: guard with exists()
if (fs.exists("config.json")) {
  const text = fs.readFile("config.json");
}

Unhandled promise rejections are silent

In script context a rejected Promise with no handler prints nothing and the process exits 0:

Promise.reject(new Error("rej"));   // no output, exit 0
async function f() { throw new Error("x"); }
f();                               // no output, exit 0

Always attach a .catch, or await inside a try. In an HTTP handler the runtime does catch it — a rejected handler Promise becomes a 500 response.

fetch rejects with strings

Failed fetches reject with a string, not an Error, so e.message is undefined:

try {
  await fetch("notaurl");
} catch (e) {
  console.log(typeof e);        // "string"
  console.log(e);               // "Invalid URL" | "Network error" | "Too many redirects"
}

A diagnostic line also goes to stderr:

[fetch] request failed: ConnectionRefused url=http://127.0.0.1:1/

Server-side failures

FailureResponse
Handler throws or its Promise rejects500 Internal Server Error
Handler never settles504 Gateway Timeout after 30 s
Response body over 10 MB500
Request body over the 4 KB read buffer413 Payload Too Large

Exit codes

CallExit code
normal end of script0
uncaught error1
process.exit(300)255 (clamped)
process.exit(-5)0 (clamped)
event-loop failure1
process.exit(300);
// $ ff exit.js; echo $?
// 255

Message catalogue

Strings you'll see in practice — useful for assertions in tests:

MessageThrown by
setTimeout requires a function as first argumenttimers
too many timers (max 128)timers
setTimeout accepts at most 8 callback argumentstimers
queueMicrotask requires a function argumentmicrotasks
too many workers (max 8)new Worker
Worker requires a module pathnew Worker
value could not be clonedpostMessage with a function/WeakMap/…
Database.open requires a file pathSQLite
exec requires a SQL string / exec: invalid databaseSQLite
transaction requires a function argumentSQLite
no database poolPostgres
sql must be used as a template tagPostgres
template arity mismatchPostgres
transaction is closedPostgres
Request requires a URL string as first argumentnew Request
Response.redirect requires a URLResponse.redirect
Invalid URLfetch, URL
fetch requires a URL string or Request as first argumentfetch
FileNotFoundfs.*Async rejection
ffi.dlopen: FFI not compiled in (build with -Dffi=true)ffi
failed to start server (is the port in use?)http.serve

Gotchas

  • e.message is undefined for fetch rejections — check typeof e.
  • A successful fs.*Async call hangs the process afterwards (it prints, then never exits) — Known issue, see fs.
  • Sync fs failures are silent; don't write try/catch around them expecting an exception.
  • There is no process.on("uncaughtException") and no unhandledRejection hook — there's nothing to subscribe to.
  • Errors thrown inside a Worker surface on the parent via onerror.

See also