| name | quickjs-memory |
| description | QuickJS-NG memory management patterns for the mikrojs runtime. Use this skill whenever working on C/C++ code that interacts with QuickJS values (JSValue), writing or reviewing native module code, debugging memory leaks or use-after-free crashes, handling promise lifecycles, writing finalizers, or any situation involving JS_DupValue/JS_FreeValue ownership. Also trigger when the user mentions refcount bugs, leaked JS values, or double-free crashes in the QuickJS layer. |
QuickJS Memory Management for mikrojs
QuickJS uses reference counting for all JS values. Every JSValue has a refcount. When the refcount reaches zero, the value is freed. Getting this wrong causes either memory leaks (forgot to free) or use-after-free crashes (freed too early). This guide covers the ownership rules used throughout the mikrojs codebase.
Core Rules
- If you receive ownership, you must eventually free it โ call
JS_FreeValue(ctx, val)
- If you want to keep a value beyond the current scope, duplicate it โ call
JS_DupValue(ctx, val)
- Some APIs consume (take ownership of) their arguments โ do NOT free after passing
- Some APIs transfer ownership to you in the return value โ you MUST free the result
API Ownership Reference
Functions that GIVE you ownership (you must free the result)
| Function | Notes |
|---|
JS_NewString(ctx, str) | New string, refcount 1 |
JS_NewObject(ctx) | New object, refcount 1 |
JS_NewArray(ctx) | New array, refcount 1 |
JS_NewInt32/64/Float64(ctx, n) | Primitives โ technically no refcount, but safe to free |
JS_NewCFunction(ctx, fn, name, argc) | New function object |
JS_NewPromiseCapability(ctx, rfuncs) | Returns promise, writes resolve/reject into rfuncs[0]/rfuncs[1] โ you own all three |
JS_GetPropertyStr(ctx, obj, prop) | Creates a new reference โ you own it |
JS_GetPropertyUint32(ctx, obj, idx) | Same โ new reference, you own it |
JS_Call(ctx, func, this, argc, argv) | You own the return value |
JS_Eval(ctx, ...) | You own the return value |
JS_ReadObject(ctx, ...) | You own the return value |
JS_NewArrayBufferCopy(ctx, buf, len) | Copies data, you own the JSValue |
JS_NewClassID(rt, &id) | Not a JSValue, but allocates a class slot |
Functions that CONSUME arguments (do NOT free after passing)
| Function | What it consumes |
|---|
JS_SetPropertyStr(ctx, obj, prop, val) | Consumes val |
JS_SetPropertyUint32(ctx, obj, idx, val) | Consumes val |
JS_DefinePropertyValueStr(ctx, obj, prop, val, flags) | Consumes val |
JS_DefinePropertyValueUint32(ctx, obj, idx, val, flags) | Consumes val |
JS_SetModuleExport(ctx, m, name, val) | Consumes val |
JS_Throw(ctx, val) | Consumes val |
JS_SetOpaque(ctx, obj, ptr) | Takes ownership of the C pointer (freed via class finalizer) |
Functions that do NOT consume and do NOT transfer ownership
| Function | Notes |
|---|
JS_Call(ctx, func, this, argc, argv) | Does NOT consume func, this, or argv elements |
JS_ToCString(ctx, val) | Returns a const char* โ free with JS_FreeCString(ctx, str), not JS_FreeValue |
JS_GetOpaque(val, class_id) | Returns a borrowed pointer โ do NOT free it |
JS_GetOpaque2(ctx, val, class_id) | Same, but throws on type mismatch |
JS_IsException(val) | Pure check, no ownership change |
JS_IsUndefined(val) / JS_IsNull(val) | Pure checks |
Common Patterns in mikrojs
Pattern 1: Setting properties on an object
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, "hello"));
JS_SetPropertyStr(ctx, obj, "count", JS_NewInt32(ctx, 42));
return obj;
Pattern 2: Reading a property, using it, freeing it
JSValue val = JS_GetPropertyStr(ctx, obj, "name");
const char* str = JS_ToCString(ctx, val);
JS_FreeCString(ctx, str);
JS_FreeValue(ctx, val);
Pattern 3: Building an array
JSValue arr = JS_NewArray(ctx);
for (int i = 0; i < count; i++) {
JS_DefinePropertyValueUint32(ctx, arr, i, JS_NewString(ctx, items[i]),
JS_PROP_C_W_E);
}
return arr;
Pattern 4: Calling a JS function from C
JSValue args[2] = { JS_NewString(ctx, "data"), JS_NewInt32(ctx, 42) };
JSValue ret = JS_Call(ctx, callback, JS_UNDEFINED, 2, args);
JS_FreeValue(ctx, args[0]);
JS_FreeValue(ctx, args[1]);
if (JS_IsException(ret)) {
js_std_dump_error(ctx);
}
JS_FreeValue(ctx, ret);
Pattern 5: Error return with cleanup
JSValue obj = JS_NewObject(ctx);
JSValue name = JS_NewString(ctx, str);
if (some_error) {
JS_FreeValue(ctx, obj);
JS_FreeValue(ctx, name);
return JS_ThrowInternalError(ctx, "something failed");
}
JS_SetPropertyStr(ctx, obj, "name", name);
return obj;
Important: If you call JS_SetPropertyStr and then hit an error, the value was already consumed โ do not free it again.
Promise Lifecycle (MIK_InitPromise / MIK_SettlePromise / MIK_FreePromise)
mikrojs wraps QuickJS promises with a MIKPromise struct (see utils.cpp):
MIKPromise p;
JSValue promise = MIK_InitPromise(ctx, &p);
JSValue result = JS_NewString(ctx, "done");
MIK_SettlePromise(ctx, &p, false, 1, &result);
MIK_FreePromise(ctx, &p);
Rules:
MIK_InitPromise stores resolve/reject at refcount 1 โ do not JS_DupValue them
MIK_SettlePromise frees the argv values, the return value of the call, and then calls MIK_FreePromise โ do not free anything yourself after calling it
- Always call either
MIK_SettlePromise or MIK_FreePromise on every code path โ never leave a MIKPromise unfreed
Class Instances and Finalizers
For C-backed JS objects (WiFi, HTTP, FileHandle), use QuickJS class IDs:
static JSClassID my_class_id;
static void my_finalizer(JSRuntime* rt, JSValue val) {
MyState* s = (MyState*)JS_GetOpaque(val, my_class_id);
if (s) {
free(s->buffer);
free(s);
}
}
static JSClassDef my_class = {
.class_name = "MyThing",
.finalizer = my_finalizer,
};
JS_NewClassID(JS_GetRuntime(ctx), &my_class_id);
JS_NewClass(JS_GetRuntime(ctx), my_class_id, &my_class);
MyState* s = (MyState*)calloc(1, sizeof(MyState));
JSValue obj = JS_NewObjectClass(ctx, my_class_id);
JS_SetOpaque(obj, s);
Finalizer rules:
- The finalizer receives
JSRuntime*, not JSContext* โ you cannot call most JS APIs
- Do NOT call
JS_FreeValue inside a finalizer โ values are being collected
- Only free C memory (
free, esp_http_client_cleanup, etc.)
- If your C struct holds
JSValue fields that need freeing during normal lifecycle, free them before the finalizer runs (e.g., in a close() method)
Storing JSValues in C Structs
When a C struct stores a JSValue (like timer callbacks or event handlers):
state->callback = JS_DupValue(ctx, callback_arg);
JS_FreeValue(ctx, state->callback);
state->callback = JS_UNDEFINED;
Use JS_UNDEFINED as the sentinel for "no value stored". Check with JS_IsUndefined() before freeing.
Top 10 Pitfalls
- Forgetting to free
JS_GetPropertyStr results โ every get creates a new ref
- Freeing a value after
JS_SetPropertyStr consumed it โ double free
- Not freeing
JS_Call return values โ even if you don't use the result
- Not freeing
JS_Call arguments โ JS_Call does NOT consume them
- Forgetting
JS_FreeCString โ ToCString allocates, must be freed separately
- Using
JS_DupValue when you already own the value โ creates a leak
- Missing cleanup on error paths โ every early return must free all owned values
- Calling JS APIs in a finalizer โ only free C memory in finalizers
- Not settling or freeing MIKPromise โ leaks resolve/reject function objects
- Storing a JSValue without JS_DupValue โ the original may be freed elsewhere