airbnb/lottie-android · error · IllegalStateException
OffscreenBitmap: finish() call without matching start()
Error message
OffscreenBitmap: finish() call without matching start()
What it means
Thrown by OffscreenLayer.finish() when any of the internal session fields (parentCanvas, op, preExistingTransform, targetRect) are null at line 350. This means finish() was called without a prior successful start(), or the start() did not complete initialization. The finish() method expects a fully initialized session and cannot safely clean up a partial or nonexistent one.
Source
Thrown at lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java:351
renderNode.setHasOverlappingRendering(true);
renderNode.setPosition((int)scaledBounds.left, (int)scaledBounds.top, (int)scaledBounds.right, (int)scaledBounds.bottom);
childCanvas = renderNode.beginRecording((int) scaledBounds.width(), (int) scaledBounds.height());
childCanvas.setMatrix(OffscreenLayer.IDENTITY_MATRIX);
childCanvas.scale(pixelScaleX, pixelScaleY); // Replicate scaling applied by parentCanvas
childCanvas.translate(-bounds.left, -bounds.top); // So that the image begins at the top-left of the bitmap
break;
default:
throw new RuntimeException("Invalid render strategy for OffscreenLayer");
}
return childCanvas;
}
public void finish() {
if (parentCanvas == null || op == null || preExistingTransform == null || targetRect == null) {
throw new IllegalStateException("OffscreenBitmap: finish() call without matching start()");
}
switch (currentStrategy) {
case DIRECT:
parentCanvas.restore();
break;
case SAVE_LAYER:
parentCanvas.restore();
break;
case BITMAP:
if (bitmap == null) {
throw new IllegalStateException("Bitmap is not ready; should've been initialized at start() time");
}
if (op.hasShadow()) {
// Composing the shadow first and then the content like this will be incorrect in theView on GitHub (pinned to 05ea92e903)
Solutions
- Track the start/finish state with a boolean flag and only call finish() if start() succeeded and hasn't been finished yet
- Ensure finish() is called exactly once per start() — use a try/finally where start() is outside the try, or guard with a flag
- If using try/finally, structure it so finish() is only called when start() completed: set a boolean after successful start()
Example fix
// before:
// try {
// layer.start(canvas, bounds, op);
// draw(layer.getCanvas());
// } finally {
// layer.finish(); // called even if start() threw, or called twice
// }
// after: guard finish() with a state flag
boolean started = false;
try {
layer.start(canvas, bounds, op);
started = true;
draw(layer.getCanvas());
} finally {
if (started) {
layer.finish();
}
} Defensive patterns
Strategy: validation
Validate before calling
// Guard finish() against missing or double calls
boolean started = false;
Canvas safeStart(OffscreenLayer layer, Canvas canvas, RectF bounds, ComposeOp op) {
Canvas result = layer.start(canvas, bounds, op);
started = true;
return result;
}
void safeFinish(OffscreenLayer layer) {
if (!started) return; // no-op if start() was never called or already finished
layer.finish();
started = false;
} Try / catch
// Guarded try/finally pattern
boolean started = false;
try {
layer.start(canvas, bounds, op);
started = true;
drawContent(layer.getCanvas());
} finally {
if (started) layer.finish();
} Prevention
- Use a boolean flag to track whether start() succeeded before calling finish()
- Never call finish() twice — the first call clears parentCanvas at line 405
- Structure try/finally so finish() is only reached when start() completed
When it happens
Trigger: Calling offscreenLayer.finish() without having called start() first, or calling finish() twice (the second call finds parentCanvas null because the first finish() cleared it at line 405). Also possible if start() itself failed partway through initialization due to an exception, leaving fields partially set. The throw is at line 351.
Common situations: A double finish() call — finish() is called, clears parentCanvas (line 405), then finish() is called again in a finally block or cleanup path. Calling finish() in a finally block when start() was never called (e.g., start() was conditionally skipped). Cleanup code that unconditionally calls finish() regardless of whether start() succeeded. Exception in start() before all fields are set.
Related errors
- Cannot nest start() calls on a single OffscreenBitmap - call
- Cannot call getKeyframes on AnimatableSplitDimensionPathValu
- Cannot interpolate between gradients. Lengths vary ({} vs {}
- Unknown trim path type {}
- Unknown point starts with {}
AI-assisted analysis of airbnb/lottie-android@05ea92e903 (2026-08-14).
Data as JSON: /api/errors/0e712a776e0fe4e6.
Report an issue: GitHub.